I was using the django_ajax library for ajax lookup in one of the form elements.
The model:
class Alpha(models.Model):
name = models.CharField()
description = models.TextField()
submitted = models.BooleanField(default=False)
The form
class MyForm(forms.Form):
alpha = AutoCompleteSelectField('alpha')
def __init__(self, loser, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.loser = loser
self.fields['alpha'].widget.attrs['class'] = 'big-text-box'
The problem with the current implementation is it shows me all the alpha entries, but in the lookup field i want only those alphas whose submitted is false.
How do I write a selector?
As is explained in the README of the project, you can achieve your goal using a custom lookup class.
Create a file lookups.py (the name is conventional) in your app directory, and define the following class in it:
from ajax_select import LookupChannel
from django.utils.html import escape
from django.db.models import Q
from yourapp.models import *
class AlphaLookup(LookupChannel):
model = Alpha
def get_query(self,q,request):
# The real query
# Here the filter will select only non-submitted entries
return Alpha.objects.filter(Q(name__icontains = q) & Q(submitted = false)).order_by('name')
def get_result(self,obj):
u""" result is the simple text that is the completion of what the person typed """
return obj.name
def format_match(self,obj):
""" (HTML) formatted item for display in the dropdown """
return escape(obj.name)
def format_item_display(self,obj):
""" (HTML) formatted item for displaying item in the selected deck area """
return escape(obj.name)
Note that raw strings should always be escaped with the escape() function in format_match and format_item_display.
The crucial thing, in your case, is the get_query method. The filter applied on Alpha.objects selects only non-submitted entries.
Do not forget to update your settings.py to use the lookup class instead of the default behavior:
AJAX_LOOKUP_CHANNELS = {
'alpha' : ('yoursite.yourapp.lookups', 'AlphaLookup'),
}
Related
I've written a custom editable table with columns subclassing from django_tables2.Column, but keep struggling with rendering a select tag in my custom column. Considering the model:
myapp/models.py
from django.db import models
from myapp.utils.enums import MyModelChoices
class MyModel(models.Model):
bound_model = models.ForeignKey(
SomeOtherModel,
related_name='bound_model'
)
used_as = models.CharField(
max_length=50,
blank=True,
null=True,
choices=MyModelChoices.choices()
)
and my enum in myapp/utils/enums.py:
class MyModelChoices:
__metaclass__ = EnumMeta # Logic irrelevant
First = 'First',
Second = 'Second',
Third = 'Third'
I end up with custom column like this:
import django_tables2 as tables
from django.forms import ChoiceField
class ChoicesColumn(tables.Column):
def __init__(self, choices, attrs=None, **extra):
self.choices = choices
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
select = ChoiceField(choices=self.choices)
return select.widget.render(
bound_column.name,
self.label_to_value(value)
)
def label_to_value(self, label):
for (v, l) in self.choices:
if l == label:
return v
which is later called in my table class like this:
import django_tables2 as tables
from myapp.models import MyModel
from myapp.tables.utils import ChoicesColumn
class MyTable(tables.Table):
name = tables.Column()
used_as = ChoicesColumn(
choices=lambda record: record.used_as.choices()
)
def render_name(self, record):
return record.bound_model.name
class Meta:
model = MyModel
fields = ('name', 'used_as',)
but still there's rendered just a plain <td></td> with text instead of select field. What am I doing wrong in this situation? I'm using Python 2.7, Django 1.8 and django-tables2 1.16.0. Thanks in advance for your advice!
UPDATE
I changed my custom column class like this:
class ChoicesColumn(tables.Column):
def __init__(self, attrs=None, **extra):
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
options = [self.render_option(c) for c in value]
html_template = '''
<select name={}>{}</select>
'''.format(bound_column.name, options)
return mark_safe(html_template)
def render_option(self, choice):
return '<option value={0}>{0}</option>'.format(choice)
and added a render_options method according to this paragraph in documentation:
class MyTable(tables.Table):
name = tables.Column(accessor='pk')
# With or without accessor it doesn't work neither way
used_as = ChoicesColumn(accessor='used_as')
def render_name(self, record):
return record.bound_model.name
def render_used_as(self, record):
return record.used_as.choices()
class Meta:
model = MyModel,
fields = ('name', 'options',)
but this method isn't even executed on render, what I've spotted while debugging, though the method before it executes when I reload the page and renders data correctly. Is that because name column uses the library class, and options column uses custom class inherited from it? If so, what is my subclass missing?
ANOTHER UPDATE
I figured out what was the previous problem with choices, though it didn't solve the problem :( The thing was that I was passing model instance's field used_as, which was set to None, thus it would never populate the ChoiceField. So, I rolled back my custom column class to the initial variant, and in my table class instead of
used_as = ChoicesColumn(
choices=lambda record: record.used_as.choices()
)
I imported MyModelChoices enum and used it instead of model instance
used_as = ChoicesColumn(choices=MyModelChoices.choices())
and now I see the options passing to constructor, though the render method isn't still called for some mysterious reason =/
LAST UPDATE AS FOR NOW
As for the current moment my custom column and table look like this:
class ChoicesColumn(tables.Column):
def __init__(self, choices, attrs=None, **extra)
self.choices = choices
self.choices.insert(0, ('', '------'))
kwargs = {'orderable': False, 'attrs': attrs}
kwargs.update(extra)
super(ChoicesColumn, self).__init__(**kwargs)
def render(self, value, bound_column):
select = forms.ChoiceField(choices=self.choices)
return select.widget.render(bound_column.name, value)
class MyTable(tables.Table):
name = tables.Column(accessor='pk')
used_as = ChoiceColumn(UsedAs.choices(), accessor='used_as')
def render_name(self, record):
return record.bound_model.name
def render_used_as(self, record):
if record.used_as is None:
return ''
return record.used_as
class Meta:
model = MyModel
fields = ('name', 'used_as')
The ChoiceColumn render method and the corresponding method in table class are never called on rendering stage (unlike the other columns), and I completely give up. Please, be merciful enough either to shoot me or tell me where exactly I'm an idiot :)
So, as I accidentally found out, the problem was in accessor attribute – when changed from
used_as = ChoiceColumn(UsedAs.choices(), accessor='used_as')
to
used_as = ChoiceColumn(UsedAs.choices(), accessor='pk')
it finally rendered. I don't understand why that happened and would be very grateful if someone explained that to me.
There is an easier way:
If you have a Enum column (say used_as), you can change the renderer so that it displays the value (instead of the name). Place this in the Table definition (in class MyTable(tables.Table) ).
def render_used_as(self,value):
v = value.split(".")[1]
members = MyModelChoices.__members__
return (members[v].value)
Note that I was using a bit of a different syntax for the Enum
from enum import Enum
Class MyModelChoices(Enum):
First = 'First'
Second = 'Second'
Third = 'Third'
Note: render_used_as is render_%s with %s = variable name
I am using django-filter and need to add a ChoiceFilter with choices dependent on the request that I receive. I am reading the docs for ChoiceFilter but it says: This filter matches values in its choices argument. The choices must be explicitly passed when the filter is declared on the FilterSet.
So is there any way to get request-dependent choices in the ChoiceFilter?
I haven't actually written the code but the following is what I want -
class F(FilterSet):
status = ChoiceFilter(choices=?) #choices depend on request
class Meta:
model = User
fields = ['status']
I've been looking too hard that I found two different ways of doing it! (both by overriding the __init__ method). Code inspired from this question.
class LayoutFilterView(filters.FilterSet):
supplier = filters.ChoiceFilter(
label=_('Supplier'), empty_label=_("All Suppliers"),)
def __init__(self, *args, **kwargs):
super(LayoutFilterView, self).__init__(*args, **kwargs)
# First Method
self.filters['supplier'].extra['choices'] = [
(supplier.id, supplier.id) for supplier in ourSuppliers(request=self.request)
]
# Second Method
self.filters['supplier'].extra.update({
'choices': [(supplier.id, supplier.name) for supplier in ourSuppliers(request=self.request)]
})
The function ourSuppliers is just to return a QuerySet to be used as choices
def ourSuppliers(request=None):
if request is None:
return Supplier.objects.none()
company = request.user.profile.company
return Supplier.objects.filter(company=company)
I am trying to generate a list to populate choices for a form. This list will be dynamic and will depend on what choices have been created by other users. Here is an example of the model the list should derive from:
#models.py
class User(models.Model):
brewery_you_work_for = models.CharField(choises=ALL_THE_BREWERIES_IN_THE_WORLD)
username = models.CharField()
I want the form to be something like this:
#forms.py
class BestBrewery(forms.Form):
vote = forms.ChoiceField(choices=BREWERIES_A_USER_WORKS_FOR)
What I want to do is have a list of all the breweries in the world for some users to select as the brewery they work for. Then I want to generate a list for other users to vote as the best brewery.
Lets say I have 3 users that claim they work the Sierra Nevada, 2 users that claim they work for Budweiser, and 5 users that claim they work for Coors.
I want to generate a list of breweries that would look like this:
(
'Budweiser',
'Coors,
'Sierra Nevada',
)
Note the alphabetical order and no repeated brewery listings.
You need to pass an extra user argument when initialising the form. Normally like this if you are using view functions:
form = BestBrewery(request.user)
If you are using Class Based Views you should override the get_form_kwargs method in your view:
def get_form_kwargs(self):
kwargs = super(MyView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
Your form should look like this:
class BestBrewery(forms.Form):
def __init__(self, user, *args, **kwargs):
super(BestBrewery, self).__init__(*args, **kwargs)
self.fields['vote'].choices = (
user.brewery_you_work_for,
user.get_brewery_you_work_for_display()
)
Just note that to override the choices field you need to provide tuples containing the value for the form and the text to be shown in the dropdown.
Well, I think u can set "choices" as result of function.
I tried like that:
def yolo():
# there we will get stats from db and return choices depends on stats
first_query = 1 # there U will use more complicated logic :)
second_query = 3
if first_query > second_query:
return (1, 2, 3)
else:
return (1, 3)
class Event(models.Model):
date = models.DateField(primary_key=True)
count_of_updates = models.SmallIntegerField(default=0)
yolos = models.CharField(max_length=255, choices=yolo())
And, for being sure U can check ur choices with something ugly:
choices = s._meta.get_field_by_name('yolos')[0].choices
print choices
Order and other things u can set in func logic :)
In your Form, you can change the choices of your field in the init.
class BestBrewery(forms.Form):
vote = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(BestBrewery, self).__init__(*arg, **kwargs)
# get the choices from where you need
choices = (...)
self.fields['vote'].choices = choices
This is a slightly simplified example of the filterset I'm using, which I'm using with the DjangoFilterBackend for Django Rest Framework. I'd like to be able to send a request to /api/bookmarks/?title__contains=word1&title__contains=word2 and have results returned that contain both words, but currently it ignores the first parameter and only filters for word2.
Any help would be very appreciated!
class BookmarkFilter(django_filters.FilterSet):
class Meta:
model = Bookmark
fields = {
'title': ['startswith', 'endswith', 'contains', 'exact', 'istartswith', 'iendswith', 'icontains', 'iexact'],
}
class BookmarkViewSet(viewsets.ModelViewSet):
serializer_class = BookmarkSerializer
permission_classes = (IsAuthenticated,)
filter_backends = (DjangoFilterBackend,)
filter_class = BookmarkFilter
ordering_fields = ('title', 'date', 'modified')
ordering = '-modified'
page_size = 10
The main problem is that you need a filter that understands how to operate on multiple values. There are basically two options:
Use MultipleChoiceFilter (not recommended for this instance)
Write a custom filter class
Using MultipleChoiceFilter
class BookmarkFilter(django_filters.FilterSet):
title__contains = django_filters.MultipleChoiceFilter(
name='title',
lookup_expr='contains',
conjoined=True, # uses AND instead of OR
choices=[???],
)
class Meta:
...
While this retains your desired syntax, the problem is that you have to construct a list of choices. I'm not sure if you can simplify/reduce the possible choices, but off the cuff it seems like you would need to fetch all titles from the database, split the titles into distinct words, then create a set to remove duplicates. This seems like it would be expensive/slow depending on how many records you have.
Custom Filter
Alternatively, you can create a custom filter class - something like the following:
class MultiValueCharFilter(filters.BaseCSVFilter, filters.CharFilter):
def filter(self, qs, value):
# value is either a list or an 'empty' value
values = value or []
for value in values:
qs = super(MultiValueCharFilter, self).filter(qs, value)
return qs
class BookmarkFilter(django_filters.FilterSet):
title__contains = MultiValueCharFilter(name='title', lookup_expr='contains')
class Meta:
...
Usage (notice that the values are comma-separated):
GET /api/bookmarks/?title__contains=word1,word2
Result:
qs.filter(title__contains='word1').filter(title__contains='word2')
The syntax is changed a bit, but the CSV-based filter doesn't need to construct an unnecessary set of choices.
Note that it isn't really possible to support the ?title__contains=word1&title__contains=word2 syntax as the widget can't render a suitable html input. You would either need to use SelectMultiple (which again, requires choices), or use javascript on the client to add/remove additional text inputs with the same name attribute.
Without going into too much detail, filters and filtersets are just an extension of Django's forms.
A Filter has a form Field, which in turn has a Widget.
A FilterSet is composed of Filters.
A FilterSet generates an inner form based on its filters' fields.
Responsibilities of each filter component:
The widget retrieves the raw value from the data QueryDict.
The field validates the raw value.
The filter constructs the filter() call to the queryset, using the validated value.
In order to apply multiple values for the same filter, you would need a filter, field, and widget that understand how to operate on multiple values.
The custom filter achieves this by mixing in BaseCSVFilter, which in turn mixes in a "comma-separation => list" functionality into the composed field and widget classes.
I'd recommend looking at the source code for the CSV mixins, but in short:
The widget splits the incoming value into a list of values.
The field validates the entire list of values by validating individual values on the 'main' field class (such as CharField or IntegerField). The field also derives the mixed in widget.
The filter simply derives the mixed in field class.
The CSV filter was intended to be used with in and range lookups, which accept a list of values. In this case, contains expects a single value. The filter() method fixes this by iterating over the values and chaining together individual filter calls.
You can create custom list field something like this:
from django.forms.widgets import SelectMultiple
from django import forms
class ListField(forms.Field):
widget = SelectMultiple
def __init__(self, field, *args, **kwargs):
super(ListField, self).__init__( *args, **kwargs)
self.field = field
def validate(self, value):
super(ListField, self).validate(value)
for val in value:
self.field.validate(val)
def run_validators(self, value):
for val in value:
self.field.run_validators(val)
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
return [self.field.to_python(val) for val in value]
and create custom filter using MultipleChoiceFilter:
class ContainsListFilter(django_filters.MultipleChoiceFilter):
field_class = ListField
def get_filter_predicate(self, v):
name = '%s__contains' % self.name
try:
return {name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {name: v}
After that you can create FilterSet with your custom filter:
from django.forms import CharField
class StorageLocationFilter(django_filters.FilterSet):
title_contains = ContainsListFilter(field=CharField())
Working for me. Hope it will be useful for you.
Here is a sample code that just works:
it supports - product?name=p1,p2,p3 and will return products with name (p1,p2,p3)
def resolve_csvfilter(queryset, name, value):
lookup = { f'{name}__in': value.split(",") }
queryset = queryset.filter(**lookup)
return queryset
class ProductFilterSet(FilterSet):
name = CharFilter(method=resolve_csvfilter)
class Meta:
model = Product
fields = ['name']
Ref: https://django-filter.readthedocs.io/en/master/guide/usage.html#customize-filtering-with-filter-method
https://github.com/carltongibson/django-filter/issues/137
I am trying to show a postgresql ArrayField as multiple input fields in a form that users can submit data.
Lets say I had a model:
class Venue(models.Model):
additional_links = ArrayField(models.URLField(validators=[URLValidator]), null=True)
That a form was using:
class VenueForm(forms.ModelForm):
class Meta:
model = Venue
exclude = ['created_date']
widgets = {
'additional_links': forms.Textarea(),
}
How would I make the ArrayField use a SplitArrayField in the ModelForm?
I tried:
class VenueForm(forms.ModelForm):
additional_links = SplitArrayField(forms.TextInput(), size=5, remove_trailing_nulls=True)
class Meta:
..
and the same in the Meta class widgets:
widgets = {
'additional_links': forms.SplitArrayField(forms.TextInput(), size=3, remove_trailing_nulls=True)
}
I also tried different form inputs/fields, but I always get the following error:
/lib/python3.5/site-packages/django/contrib/postgres/forms/array.py", line 155, in __init__
widget = SplitArrayWidget(widget=base_field.widget, size=size)
AttributeError: 'TextInput' object has no attribute 'widget'
The SplitArrayField and SplitArrayWidget it is very different things.
Usage SplitArrayField in forms.ModelForm is adding a new field to a form.
The SplitArrayWidget is default widget for SplitArrayField, so you don`t need change it.
A problem is if you need set this widget to the field "additional_links", because value for the ArrayField must be comma-separated values without spaces.
Example:
https://www.google.com,https://www.google.com.ru,https://www.google.com.ua
Hence, the Django use django.forms.widgets.TextInput by default for ArrayField. It is very no comfortably for humans.
But if you still want use the SplitArrayWidget for the ArrayField you need modified this widget.
My version:
from django.contrib import postgres
class SplitInputsArrayWidget(postgres.forms.SplitArrayWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def value_from_datadict(self, data, files, name):
value_from_datadict = super().value_from_datadict(data, files, name)
# convert a list to a string, with commas-separated values
value_from_datadict = ','.join(value_from_datadict)
return value_from_datadict
def render(self, name, value, attrs=None):
# if object has value, then
# convert a sting to a list by commas between values
if value is not None:
value = value.split(',')
return super().render(name, value, attrs=None)
How to use it:
Model:
class Article(models.Model):
"""
Model for article
"""
links = ArrayField(
models.URLField(max_length=1000),
size=MAX_COUNT_LINKS,
verbose_name=_('Links'),
help_text=_('Useful links'),
)
Form:
class ArticleAdminModelForm(forms.ModelForm):
"""
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# field 'links'
widget_base_field = self.fields['links'].base_field.widget
count_inputs = self.fields['links'].max_length
self.fields['links'].widget = SplitInputsArrayWidget(
widget_base_field,
count_inputs,
attrs={'class': 'span12'}
)
Result in the admin:
Django 1.10 (skin Django-Suit)
Python 3.4
Useful links in web:
https://docs.djangoproject.com/en/1.10/_modules/django/contrib/postgres/forms/array/#SplitArrayField
https://bradmontgomery.net/blog/nice-arrayfield-widgets-choices-and-chosenjs/
More my widgets is in my new Django-project (see a file https://github.com/setivolkylany/programmerHelper/blob/master/utils/django/widgets.py)
Unfortunately right now this code without tests, so late I will update my answer.