django-filter with django autocomplete light - django

Has anyone succesfully used dal and django-filter together?
Below attempt is mine,
I tried to use filterset_factory, supplying model class and fields list, then I tried to use futuremodelform.
I got ,
ModelForm has no model class specified.
I think it's just one of many errors to occur.
Anybody done that before, I have to use filterset_factory, and create dynamic classes from arguments, I also want to override widgets so dal widgets can be used.
#testing filterset
from dal import autocomplete
from django.db import models
class PanFilterSet(django_filters.FilterSet):
filter_overrides = {
models.ForeignKey: {
'filter_class': autocomplete.ModelSelect2,
},
}
def pan_filterset_factory(model,fields):
meta = type(str('Meta'), (object,), {'model': model,'fields':fields,'form':autocomplete.FutureModelForm})
filterset = type(str('%sFilterSet' % model._meta.object_name),
(PanFilterSet,), {'Meta': meta})
return filterset
searchFormFilterSet = pan_filterset_factory(self.model_class,self.final_search_fields)
f = searchFormFilterSet(self.request.GET, queryset=self.get_queryset())
print f.form.as_p()

I'm not very familiar with DAL, but I contribute to django-filter and have a decent understanding of its internals. A few notes:
The filter_class in your filter_overrides should be a filter, not a widget. You can provide additional arguments (such as the widget) through the extra key, as seen here. Any parameter that does not belong to the filter is automatically passed to the underlying form field.
Using an override isn't the right approach anyway, as the widget needs a field-specific endpoint to perform autocompletion. Since the endpoint is field-specific, it's not applicable to all ForeignKeys.
django-filter uses regular Forms, not ModelForms, so an appropriate Meta inner class would not be constructed. FutureModelForm doesn't seem to provide autocomplete functionality anyway - it seems irrelevant?
Your factory will have to generate your autocomplete filters manually - something like the following:
def dal_field(field_name, url):
return filters.ModelChoiceFilter(
name=field_name,
widget=autocomplete.ModelSelect2(url=url),
)
def dal_filterset_factory(model, fields, dal_fields):
attrs = {field: dal_field(field, url) for field, url in dal_fields.items()}
attrs['Meta'] = type(str('Meta'), (object,), {'model': model,'fields': fields})
filterset = type(str('%sFilterSet' % model._meta.object_name),
(FilterSet,), attrs)
return filterset
# Usage:
# mapping of {field names: autocomplete endpoints}.
dal_fields = {'birth_country': 'country-autocomplete'}
fields = ['list', 'or', 'dict', 'of', 'other', 'fields']
SomeModelFilterSet = dal_filterset_factory(SomeModel, fields, dal_fields)
The fields in attrs use the declarative API. More info in the docs.

Related

Django: from request.GET to QuerySet

I know how to create a html form with Django and I know how to access the values in request.GET.
I know how to use the ORM: MyModel.objects.filter(...)
Now I a missing how get from request.GET to a matching QuerySet
I could not find this in the Django docs.
Example:
class MyModel(models.Model):
name=models.CharField(max_length=1024)
if request.GET contains name=foo I would like to have a filter like MyModel.objects.filter(name__icontains=request.GET['name']).
I could code this on my own, but I think this is re-inventing the wheel. It feels too complicated to me.
Above is just one example. I prefer a solution which is more configuring than coding.
We can here construct a Q object:
from django.db.models import Q
MyModel.objects.filter(
Q([('{}__icontains'.format(k), v) for k, vs in request.GET.lists() for v in vs])
)
This will work as well given a certain key occurs multiple times in the querystring. Note that the fields you query over should support an __icontains [Django-doc] lookup.
Furthermore it might be unsafe to allow that, since for example a hacker could try to use user__password to make guesses on the (hashed) password of a user.
You should do it this way
filters = {}
for param, value in request.GET.items():
filters['{}_icontains'.format(param)] = value
queryset = MyModel.objects.filter(**filters)
Reference https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.QueryDict.items
You can use django-tables2 and django-filter
Explained here:
Django-filter can be used for generating interfaces similar to
the Django admin’s list_filter interface. It has an API very similar
to Django’s ModelForms. For example, if you had a Product model you
could have a filterset for it with the code:
import django_filters
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['name', 'price', 'manufacturer']
And then in your view you could do:
def product_list(request):
filter = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': filter})
How to use django-filter in django-tables2, see django-filter in django-table2

DRF show ForeignKey field choices

What's the best way to go about letting a frontend user know about ForeignKey field choices using Django Rest Framework? In the Browsable API these fields have a dropdown widget with all the existing objects as choices.
A custom metadata class could return the available choices for each field but the request could be very slow if there are millions of objects.
Suppose you have a model similar to below and there's only 5 unit objects. How would you go about listing the unit choices?
class OrderLine(models.Model):
order = models.ForeignKey(Order)
product = models.ForeignKey(Product)
unit = models.ForeignKey(Unit)
I ended up implementing a custom metadata class that adds foreign key choices to an OPTIONS request based on the serializer attribute extra_choice_fields. This way you can choose which fields to provided choices for on each serializer and which to not include (ex. exclude fields with a lot of objects).
from rest_framework.metadata import SimpleMetadata
from rest_framework.relations import ManyRelatedField, RelatedField
from django.utils.encoding import force_text
class ChoicesMetadata(SimpleMetadata):
def get_field_info(self, field):
field_info = super().get_field_info(field)
if (isinstance(field, (RelatedField, ManyRelatedField)) and
field.field_name in getattr(field.parent.Meta, 'extra_choice_fields', [])):
field_info['choices'] = [{
'value': choice_value,
'display_name': force_text(choice_name, strings_only=True)
} for choice_value, choice_name in field.get_choices().items()]
return field_info

Django admin: don't send all options for a field?

One of my Django admin "edit object" pages started loading very slowly because of a ForeignKey on another object there that has a lot of instances. Is there a way I could tell Django to render the field, but not send any options, because I'm going to pull them via AJAX based on a choice in another SelectBox?
You can set the queryset of that ModelChoiceField to empty in your ModelForm.
class MyAdminForm(forms.ModelForm):
def __init__(self):
self.fields['MY_MODEL_CHOIE_FIELD'].queryset = RelatedModel.objects.empty()
class Meta:
model = MyModel
fields = [...]
I think you can try raw_id_fields
By default, Django’s admin uses a select-box interface () for fields that are ForeignKey. Sometimes you don’t want to incur the overhead of having to select all the related instances to display in the drop-down.
raw_id_fields is a list of fields you would like to change into an Input widget for either a ForeignKey or ManyToManyField
Or you need to create a custom admin form
MY_CHOICES = (
('', '---------'),
)
class MyAdminForm(forms.ModelForm):
my_field = forms.ChoiceField(choices=MY_CHOICES)
class Meta:
model = MyModel
fields = [...]
class MyAdmin(admin.ModelAdmin):
form = MyAdminForm
Neither of the other answers worked for me, so I read Django's internals and tried on my own:
class EmptySelectWidget(Select):
"""
A class that behaves like Select from django.forms.widgets, but doesn't
display any options other than the empty and selected ones. The remaining
ones can be pulled via AJAX in order to perform chaining and save
bandwidth and time on page generation.
To use it, specify the widget as described here in "Overriding the
default fields":
https://docs.djangoproject.com/en/1.9/topics/forms/modelforms/
This class is related to the following StackOverflow problem:
> One of my Django admin "edit object" pages started loading very slowly
> because of a ForeignKey on another object there that has a lot of
> instances. Is there a way I could tell Django to render the field, but
> not send any options, because I'm going to pull them via AJAX based on
> a choice in another SelectBox?
Source: http://stackoverflow.com/q/37327422/1091116
"""
def render_options(self, *args, **kwargs):
# copy the choices so that we don't risk affecting validation by
# references (I hadn't checked if this works without this trick)
choices_copy = self.choices
self.choices = [('', '---------'), ]
ret = super(EmptySelectWidget, self).render_options(*args, **kwargs)
self.choices = choices_copy
return ret

Can I disable a field in the Rest Framework API browsing view

I am using Django Rest Framework to serialize a model in which I have a foreignkey.
models.py
class Article(models.Model):
author = models.ForeignKey(Author, related_name='articles')
... other fields...
serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Article
I want to get rid of the 'HTML form' at the bottom of the browsable API view since I get a list with all my articles and retrieving them from the DB takes ages (I have some 100K articles, and each time the html form is displayed, my server does 100K queries).
I have read the answer from How to disable admin-style browsable interface of django-rest-framework? and I am currently displaying the view in JSON. However, I like the html view and would like to find a way to avoid the html form available at the bottom.
I don't want to properly remove the field from the view (I need to use it), but just remove the database queries used to populate the form.
Any idea ?
Making the field read-only also means you cannot modify it, which is probably not wanted in all cases.
Another solution is to override the BrowsableApiRenderer so it won't display the HTML form (which can be indeed really slow with a lot of data).
This is surprisingly easy, just override get_rendered_html_form:
from rest_framework.renderers import BrowsableAPIRenderer
class NoHTMLFormBrowsableAPIRenderer(BrowsableAPIRenderer):
def get_rendered_html_form(self, *args, **kwargs):
"""
We don't want the HTML forms to be rendered because it can be
really slow with large datasets
"""
return ""
then adjust your settings to use this renderer:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'myapp.renderers.NoHTMLFormBrowsableAPIRenderer',
)
}
I answer my own question.
I found in the documentation the solution to my problem. I had to use the read_only attribute.
serializers.py
class ArticleSerializer(serializers.HyperlinkedModelSerializer):
author = serializers.RelatedField(read_only=True)
class Meta:
model = Article
fields = ('author', ...other_fields)
#maerteijn answer will disable all forms: POST, PUT, DELETE and OPTIONS.
If you still want to allow the awesome "OPTIONS" button, you can do something like this
class NoHTMLFormBrowsableAPIRenderer(BrowsableAPIRenderer):
OPTIONS_METHOD = "OPTIONS"
def get_rendered_html_form(self, data, view, method, request):
if method == self.OPTIONS_METHOD:
return super().get_rendered_html_form(data, view, method, request)
else:
"""
We don't want the HTML forms to be rendered because it can be
really slow with large datasets
"""
return ""
And modify settings.py in the same way
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'myapp.renderers.NoHTMLFormBrowsableAPIRenderer',
)
}

django-autocomplete-light filter queryset

I am trying to use django-autocomplete-light but I have some problems.
I would like to filter the queryset in the ModelChoiceField.
If I don't use auto-complete my result selection is correct but if I use widget it doesn't work correctly, it shows all records.
Here is my code:
class MyModelAdminForm(forms.ModelForm):
def __init__(self, *args, **kw):
super(MyModelAdminForm, self).__init__(*args, **kw)
self.fields['my_field'] = forms.ModelChoiceField(
MyModel.objects.filter(status=1),
widget=autocomplete_light.ChoiceWidget('MyModelAutocomplete')
)
class MyModelAdmin(ModelAdmin):
form = MyModelAdminForm
You should set MyModelAutocomplete.choices, either via register():
autocomplete_light.register(MyModel, choices=MyModel.objects.filter(status=1))
Or within the class:
class MyModelAutocomplete(autocomplete_light.AutocompleteModelBase):
choices = MyModel.objects.filter(status=1)
Refer to docs for more:
AutocompleteModel API docs
Using register() to pass class attributes: "In addition, keyword arguments will be set as class attributes."
Overriding choices_for_request() might be useful if you need to filter choices based on the user.
I would like to automate this, but the widget isn't aware about the form field instance unfortunately.
Apply the filter inside MyModelAutocomplete by defining a method
class MyModelAutocomplete(autocomplete_light.AutocompleteModelBase):
choices=MyModel.objects.all()
def choices_for_request(self):
choices = choices.filter(status=1)
return self.order_choices(choices)[0:self.limit_choices]
choices_for_request is mostly used for dynamic filterming
I was trying to figure out how to do this within the autocomplete-light documentation. I figured out how, but not without a bit of digging, so hopefully this is helpful.
In the autocomplete_light_registry.py file, fill out the "name" and "choices" parameters:
#myapp/autocomplete_light_registry.py
autocomplete_light.register(MyModel,
#... Other Parameters ...
name = 'SpecialMyModelAutocomplete',
choices = YourQuerySetHere, #e.g. MyModel.objects.filter(...)
)
The default name is "MyModelAutocomplete" so if you include more than one registered autocomplete for a model, you need to specify which one you want to use (otherwise it uses the first one in the registry, NOT the default).
To specify, use "autocomplete_names" which is (from the docs) "A dict of field_name: AutocompleteName to override the default autocomplete that would be used for a field." In my case I'm using it within the django admin.
#myapp/admin.py
class MyModelAdminForm(autocompletelight.ModelForm):
class Meta:
model = MyModel
autocomplete_names = {'field_name':'SpecialMyModelAutocomplete'}
Note that you don't need to include any fields for which you want to use the default Autocomplete in autocomplete_names. Incidentally "autocomplete_exclude" and "autocomplete_fields" may also be of interest here and are analogous to "fields" and "exclude" in a ModelAdmin to specify which fields to include/exclude from using autocomplete.
Addition:
You can also use "autocomplete_names" in the modelform_factory:
form = autocomplete_light.modelform_factory(MyOtherModel,autocomplete_names={MyFieldName:'MyModelAutocomplete'}) #where MyFieldName is a ForeignKey to MyModel
From the docs:
autocomplete_light.forms.modelform_factory(model,autocomplete_fields=None,autocomplete_exclude=None,autocomplete_names=None,registry=None,**kwargs)