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.
Related
When I render the form to a HTML template,
a certain field of form which is initiated by init is ordered always at bottom of table, even though I defined the field at the middle of form class.
Is there any way or method to customize the order of fields in the form where a initiated field exists by init.
I wanna put the field in the middle of form table in HTML template.
A screenshot of the rendered template:
In the screenshot,
the field "category_name" is ordered at the bottom of tag
I wanna change the order to the middle of table.
I am using Django 2.2 and python 3.7 on Windows 10.
Thanks
from django import forms
from .models import Category_name
class MakePurchaseLog(forms.Form):
buy_date = forms.DateField(input_formats=['%Y-%m-%d'])
shop = forms.CharField(max_length=50)
def __init__(self, user, *args, **kwargs):
super(MakePurchaseLog, self).__init__(*args, **kwargs)
self.fields['category_name'] = forms.ChoiceField(
choices = [(item.category_name, item.category_name) \
for item in Category_name.objects. \
filter(owner=user)])
goods_name = forms.CharField(max_length=50)
price = forms.IntegerField(min_value=0)
memo = forms.CharField(max_length=50, required=False)
field_order = ['category_name']
The fact that the __init__ is placed in the middle of the class, will not make any difference, since this is a function, and the evaluation is thus "postponed" until you actually create a MakePurchaseLog form object.
I think the most elegant solution here is to just already define the field in your form, and then in the __init__ function alter the choices, like:
class MakePurchaseLog(forms.Form):
buy_date = forms.DateField(input_formats=['%Y-%m-%d'])
shop = forms.CharField(max_length=50)
category_name = forms.ChoiceField()
goods_name = forms.CharField(max_length=50)
price = forms.IntegerField(min_value=0)
memo = forms.CharField(max_length=50, required=False)
def __init__(self, user, *args, **kwargs):
super(MakePurchaseLog, self).__init__(*args, **kwargs)
self.fields['category_name'].choices = [
(item.category_name, item.category_name)
for item in Category_name.objects.filter(owner=user)
]
We thus populate the choices of the category_name field in the __init__ function, but we define the field already at the class level.
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'm trying to change the widget to fields in a form which have a string in the name, I'm trying to do something like the following:
class CI_tableForm(ModelForm):
class Meta:
model = CI_table
fields = report_query_values
for field in report_query_values:
if "_id" in field:
field = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple)
Not sure if it's possible or not.
At the moment it doesn't error, but doesn't change the widget either.
Thanks,
Isaac
You should do it in the __init__ constructor:
class CI_tableForm(ModelForm):
class Meta:
model = CI_table
fields = report_query_values
def __init__(self, *args, **kwargs):
super(CI_tableForm, self).__init__(*args, **kwargs)
for field in report_query_values:
if "_id" in field:
choices = self.fields[field].widget.choices
self.fields[field].widget = forms.CheckboxSelectMultiple(
choices=choices)
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'),
}
My form field looks something like the following:
class FooForm(ModelForm):
somefield = models.CharField(
widget=forms.TextInput(attrs={'readonly':'readonly'})
)
class Meta:
model = Foo
Geting an error like the following with the code above: init() got an unexpected keyword argument 'widget'
I thought this is a legitimate use of a form widget?
You should use a form field and not a model field:
somefield = models.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
replaced with
somefield = forms.CharField(
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
Should fix it.
Note that the readonly attribute does not keep Django from processing any value sent by the client. If it is important to you that the value doesn't change, no matter how creative your users are with FireBug, you need to use a more involved method, e.g. a ReadOnlyField/ReadOnlyWidget like demonstrated in a blog entry by Alex Gaynor.
I was going into the same problem so I created a Mixin that seems to work for my use cases.
class ReadOnlyFieldsMixin(object):
readonly_fields =()
def __init__(self, *args, **kwargs):
super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
for field in (field for name, field in self.fields.iteritems() if name in self.readonly_fields):
field.widget.attrs['disabled'] = 'true'
field.required = False
def clean(self):
cleaned_data = super(ReadOnlyFieldsMixin,self).clean()
for field in self.readonly_fields:
cleaned_data[field] = getattr(self.instance, field)
return cleaned_data
Usage, just define which ones must be read only:
class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
readonly_fields = ('field1', 'field2', 'fieldx')
As Benjamin (https://stackoverflow.com/a/2359167/565525) nicely explained, additionally to rendering correctly, you need to process field on backend properly.
There is an SO question and answers that has many good solutions. But anyway:
1) first approach - removing field in save() method, e.g. (not tested ;) ):
def save(self, *args, **kwargs):
for fname in self.readonly_fields:
if fname in self.cleaned_data:
del self.cleaned_data[fname]
return super(<form-name>, self).save(*args,**kwargs)
2) second approach - reset field to initial value in clean method:
def clean_<fieldname>(self):
return self.initial[<fieldname>] # or getattr(self.instance, <fieldname>)
Based on second approach I generalized it like this:
from functools import partial
class <Form-name>(...):
def __init__(self, ...):
...
super(<Form-name>, self).__init__(*args, **kwargs)
...
for i, (fname, field) in enumerate(self.fields.iteritems()):
if fname in self.readonly_fields:
field.widget.attrs['readonly'] = "readonly"
field.required = False
# set clean method to reset value back
clean_method_name = "clean_%s" % fname
assert clean_method_name not in dir(self)
setattr(self, clean_method_name, partial(self._clean_for_readonly_field, fname=fname))
def _clean_for_readonly_field(self, fname):
""" will reset value to initial - nothing will be changed
needs to be added dynamically - partial, see init_fields
"""
return self.initial[fname] # or getattr(self.instance, fname)