Django: MultiValueField and MultiWidget - python

Django's documentation doesn't do a very thorough job of explaining how to use MultiValueField and MultiWidget. I've tried dissecting the one implementation and haven't had good results. Would someone mind giving me a quick pointer in the right direction?
My example:
widgets.py
from django import forms
class TestMultiWidget(forms.MultiWidget):
def __init__(self, attrs=None):
widgets = (
forms.TextInput(attrs=attrs),
forms.TextInput(attrs=attrs),
)
super(TestMultiWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return value.split(':::')[0:2]
return ['', '']
fields.py
from django import forms
from widgets import TestMultiWidget
class TestMultiField(forms.MultiValueField):
widget = TestMultiWidget
def __init__(self, *args, **kwargs):
fields = (
forms.CharField(),
forms.CharField(),
)
super(TestMultiField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
return ':::'.join(data_list)
return ''
models.py
from django.db import models
from util.fields import TestMultiField
class Test(models.Model):
a = models.CharField(max_length=128)
b = TestMultiField()
c = models.CharField(max_length=128)
admin.py
from django.contrib import admin
from models import Test
admin.site.register(Test)
And the resulting admin.
Anybody have a clue what's happening here? My guess is that there's some unintended exception suppression happening, but I haven't been able to locate the source.
Thanks!

Please notice that django.forms.MultiValueField is a form field and not a model field (like django.db.models.CharField). Therefore, it is not treated as a model field in your Test model, and was not created in your database. (You can check it with ./manage.py sqlall myapp).
Change your models.py to:
from django.db import models
from fields import TestMultiField
class TestMultiModelField(models.Field):
def formfield(self, **kwargs):
defaults = {'form_class': TestMultiField}
defaults.update(kwargs)
return super(TestMultiModelField, self).formfield(**defaults)
def get_internal_type(self):
return 'TextField'
class Test(models.Model):
a = models.CharField(max_length=128)
b = TestMultiModelField()
c = models.CharField(max_length=128)
drop your table (on linux/mac: ./manage.py sqlclear myapp | ./manage.py dbshell) and syncdb to create your table, this time with column b. Check your admin now.
Explanation:
To create a custom model field, follow this: https://docs.djangoproject.com/en/dev/howto/custom-model-fields/
The set the model field's matching form field, the formfield method was used.
(BTW, The "correct" way to design the model field is probably a bit different, using to_python and get_prep_value)

Related

Django ModelMultipleChoiceField lazy loading of related m2m objects

I tried to use django.forms.ModelMultipleChoiceField in one of the model's admin page in order to update m2m relation through form. I also specified django.contrib.admin.widgets.FilteredSelectMultiple as a widget for form's field and achieved my purpose successfully. The problem is, since I have over 10K related objects, all of them are included as an option in form page and that makes page very severely slow. For illustrating this problem I am adding here analogy of this case's appliance on django.contrib.auth.models.Group model and related 'users'.
admin.py
from django.contrib import admin
from django.contrib.auth.models import Group
from forms import GroupAdminForm
admin.site.unregister(Group) # get rid of default one
class GroupAdmin(admin.ModelAdmin):
form = GroupAdminForm
filter_horizontal = ['permissions']
admin.site.register(Group, GroupAdmin)
forms.py
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Group
User = get_user_model()
class GroupAdminForm(forms.ModelForm):
class Meta:
model = Group
exclude = []
users = forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,
widget=FilteredSelectMultiple('users', False)
)
def __init__(self, *args, **kwargs):
super(GroupAdminForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.fields['users'].initial = self.instance.user_set.all()
def save_m2m(self):
self.instance.user_set.set(self.cleaned_data['users'])
def save(self, *args, **kwargs):
instance = super(GroupAdminForm, self).save()
self.save_m2m()
return instance
In the codes above, default admin change page for Group is disabled and implemented again that also includes input for group users. This is how it looks after I added 1000 test user objects:
As you see, all of 1000 users are included as an option. My question is that, how can I achieve to disable displaying them initially and display as the user types something in filter box?
Note: Please don't suggest me to use admin.ModelAdmin.raw_id_fields, since that is not user-friendly. Also, it would be better to solve this issue without help of any third-party package.

Add custom form fields that are not part of the model (Django)

I have a model registered on the admin site. One of its fields is a long string expression. I'd like to add custom form fields to the add/update pages of this model in the admin. Based on the values of these fields I will build the long string expression and save it in the relevant model field.
How can I do this?
I'm building a mathematical or string expression from symbols. The user chooses symbols (these are the custom fields that are not part of the model) and when they click save then I create a string expression representation from the list of symbols and store it in the DB. I don't want the symbols to be part of the model and DB, only the final expression.
Either in your admin.py or in a separate forms.py you can add a ModelForm class and then declare your extra fields inside that as you normally would. I've also given an example of how you might use these values in form.save():
from django import forms
from yourapp.models import YourModel
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# ...do something with extra_field here...
return super(YourModelForm, self).save(commit=commit)
class Meta:
model = YourModel
To have the extra fields appearing in the admin just:
Edit your admin.py and set the form property to refer to the form you created above.
Include your new fields in your fields or fieldsets declaration.
Like this:
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
fieldsets = (
(None, {
'fields': ('name', 'description', 'extra_field',),
}),
)
UPDATE:
In Django 1.8 you need to add fields = '__all__' to the metaclass of YourModelForm.
It it possible to do in the admin, but there is not a very straightforward way to it. Also, I would like to advice to keep most business logic in your models, so you won't be dependent on the Django Admin.
Maybe it would be easier (and maybe even better) if you have the two seperate fields on your model. Then add a method on your model that combines them.
For example:
class MyModel(models.model):
field1 = models.CharField(max_length=10)
field2 = models.CharField(max_length=10)
def combined_fields(self):
return '{} {}'.format(self.field1, self.field2)
Then in the admin you can add the combined_fields() as a readonly field:
class MyModelAdmin(models.ModelAdmin):
list_display = ('field1', 'field2', 'combined_fields')
readonly_fields = ('combined_fields',)
def combined_fields(self, obj):
return obj.combined_fields()
If you want to store the combined_fields in the database you could also save it when you save the model:
def save(self, *args, **kwargs):
self.field3 = self.combined_fields()
super(MyModel, self).save(*args, **kwargs)
Django 2.1.1
The primary answer got me halfway to answering my question. It did not help me save the result to a field in my actual model. In my case I wanted a textfield that a user could enter data into, then when a save occurred the data would be processed and the result put into a field in the model and saved. While the original answer showed how to get the value from the extra field, it did not show how to save it back to the model at least in Django 2.1.1
This takes the value from an unbound custom field, processes, and saves it into my real description field:
class WidgetForm(forms.ModelForm):
extra_field = forms.CharField(required=False)
def processData(self, input):
# example of error handling
if False:
raise forms.ValidationError('Processing failed!')
return input + " has been processed"
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# self.description = "my result" note that this does not work
# Get the form instance so I can write to its fields
instance = super(WidgetForm, self).save(commit=commit)
# this writes the processed data to the description field
instance.description = self.processData(extra_field)
if commit:
instance.save()
return instance
class Meta:
model = Widget
fields = "__all__"
You can always create new admin template, and do what you need in your admin_view (override the admin add URL to your admin_view):
url(r'^admin/mymodel/mymodel/add/$','admin_views.add_my_special_model')
If you absolutely only want to store the combined field on the model and not the two seperate fields, you could do something like this:
Create a custom form using the form attribute on your ModelAdmin. ModelAdmin.form
Parse the custom fields in the save_formset method on your ModelAdmin. ModelAdmin.save_model(request, obj, form, change)
I never done something like this so I'm not completely sure how it will work out.
The first (highest score) solution (https://stackoverflow.com/a/23337009/10843740) was accurate, but I have more.
If you declare fields by code, that solution works perfectly, but what if you want to build those dynamically?
In this case, creating fields in the __init__ function for the ModelForm won't work. You will need to pass a custom metaclass and override the declared_fields in the __new__ function!
Here is a sample:
class YourCustomMetaClass(forms.models.ModelFormMetaclass):
"""
For dynamically creating fields in ModelForm to be shown on the admin panel,
you must override the `declared_fields` property of the metaclass.
"""
def __new__(mcs, name, bases, attrs):
new_class = super(NamedTimingMetaClass, mcs).__new__(
mcs, name, bases, attrs)
# Adding fields dynamically.
new_class.declared_fields.update(...)
return new_class
# don't forget to pass the metaclass
class YourModelForm(forms.ModelForm, metaclass=YourCustomMetaClass):
"""
`metaclass=YourCustomMetaClass` is where the magic happens!
"""
# delcare static fields here
class Meta:
model = YourModel
fields = '__all__'
This is what I did to add the custom form field "extra_field" which is not the part of the model "MyModel" as shown below:
# "admin.py"
from django.contrib import admin
from django import forms
from .models import MyModel
class MyModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# Do something with extra_field here
return super().save(commit=commit)
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
You might get help from my answer at :
my response previous on multicheckchoice custom field
You can also extend multiple forms having different custom fields and then assigning them to your inlines class like stackedinline or tabularinline:
form =
This way you can avoid formset complication where you need to add multiple custom fields from multiple models.
so your modeladmin looks like:
inlines = [form1inline, form2inline,...]
In my previous response to the link here, you will find init and save methods.
init will load when you view the page and save will send it to database.
in these two methods you can do your logic to add strings and then save thereafter view it back in Django admin change_form or change_list depending where you want.
list_display will show your fields on change_list.
Let me know if it helps ...
....
class CohortDetailInline3(admin.StackedInline):
model = CohortDetails
form = DisabilityTypesForm
...
class CohortDetailInline2(admin.StackedInline):
model = CohortDetails
form = StudentRPLForm
...
...
#admin.register(Cohort)
class CohortAdmin(admin.ModelAdmin):
form = CityInlineForm
inlines = [uploadInline, cohortDetailInline1,
CohortDetailInline2, CohortDetailInline3]
list_select_related = True
list_display = ['rto_student_code', 'first_name', 'family_name',]
...

(Django) Trim whitespaces from charField

How do I strip whitespaces (trim) from the end of a charField in Django?
Here is my Model, as you can see I've tried putting in clean methods but these never get run.
I've also tried doing name.strip(), models.charField().strip() but these do not work either.
Is there a way to force the charField to trim automatically for me?
Thanks.
from django.db import models
from django.forms import ModelForm
from django.core.exceptions import ValidationError
import datetime
class Employee(models.Model):
"""(Workers, Staff, etc)"""
name = models.CharField(blank=True, null=True, max_length=100)
def save(self, *args, **kwargs):
try:
# This line doesn't do anything??
#self.full_clean()
Employee.clean(self)
except ValidationError, e:
print e.message_dict
super(Employee, self).save(*args, **kwargs) # Real save
# If I uncomment this, I get an TypeError: unsubscriptable object
#def clean(self):
# return self.clean['name'].strip()
def __unicode__(self):
return self.name
class Meta:
verbose_name_plural = 'Employees'
class Admin:pass
class EmployeeForm(ModelForm):
class Meta:
model = Employee
# I have no idea if this method is being called or not
def full_clean(self):
return super(Employee), self.clean().strip()
#return self.clean['name'].strip()
Edited: Updated code to my latest version. I am not sure what I am doing wrong as it's still not stripping the whitespace (trimming) the name field.
When you're using a ModelForm instance to create/edit a model, the model's clean() method is guaranteed to be called. So, if you want to strip whitespace from a field, you just add a clean() method to your model (no need to edit the ModelForm class):
class Employee(models.Model):
"""(Workers, Staff, etc)"""
name = models.CharField(blank=True, null=True, max_length=100)
def clean(self):
if self.name:
self.name = self.name.strip()
I find the following code snippet useful- it trims the whitespace for all of the model's fields which subclass either CharField or TextField (so this also catches URLField fields) without needing to specify the fields individually:
def clean(self):
for field in self._meta.fields:
if isinstance(field, (models.CharField, models.TextField)):
value = getattr(self, field.name)
if value:
setattr(self, field.name, value.strip())
Someone correctly pointed out that you should not be using null=True in the name declaration. Best practice is to avoid null=True for string fields, in which case the above simplifies to:
def clean(self):
for field in self._meta.fields:
if isinstance(field, (models.CharField, models.TextField)):
setattr(self, field.name, getattr(self, field.name).strip())
Model cleaning has to be called (it's not automatic) so place some self.full_clean() in your save method.
http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.full_clean
As for your form, you need to return the stripped cleaned data.
return self.cleaned_data['name'].strip()
Somehow I think you just tried to do a bunch of stuff that doesn't work. Remember that forms and models are 2 very different things.
Check up on the forms docs on how to validate forms
http://docs.djangoproject.com/en/dev/ref/forms/validation/
super(Employee), self.clean().strip() makes no sense at all!
Here's your code fixed:
class Employee(models.Model):
"""(Workers, Staff, etc)"""
name = models.CharField(blank=True, null=True, max_length=100)
def save(self, *args, **kwargs):
self.full_clean() # performs regular validation then clean()
super(Employee, self).save(*args, **kwargs)
def clean(self):
"""
Custom validation (read docs)
PS: why do you have null=True on charfield?
we could avoid the check for name
"""
if self.name:
self.name = self.name.strip()
class EmployeeForm(ModelForm):
class Meta:
model = Employee
def clean_name(self):
"""
If somebody enters into this form ' hello ',
the extra whitespace will be stripped.
"""
return self.cleaned_data.get('name', '').strip()
If you have so many data-fields to be trimmed, why not try extending CharField?
from django.db import models
from django.utils.translation import ugettext_lazy as _
class TrimCharField(models.CharField):
description = _(
"CharField that ignores leading"
" and trailing spaces in data")
def get_prep_value(self, value)
return trim(super(TrimCharField, self
).get_prep_value(value))
def pre_save(self, model_instance, add):
return trim(super(TrimCharField, self
).pre_save(model_instance, add))
UPDATE:
For Django versions <= 1.7 if you want to extend field, you are to use models.SubfieldBase metaclass. So here it will be like:
class TrimCharField(six.with_metaclass(
models.SubfieldBase, models.CharField)):
Django 1.9 offers a simple way of accomplishing this. By using the strip argument whose default is True, you can make sure that leading and trailing whitespace is trimmed. You can only do that in form fields though in order to make sure that user input is trimmed. But that still won't protect the model itself. If you still want to do that, you can use any of the methods above.
For more information, visit https://docs.djangoproject.com/en/1.9/ref/forms/fields/#charfield
I'm handling this in views as a decorator. I'm also truncating field values that exceed a CharField max_length value.
from django import forms
from django import models
from django.db.models.fields import FieldDoesNotExist
from django.utils.encoding import smart_str
class CleanCharField(forms.CharField):
"""Django's default form handling drives me nuts wrt trailing
spaces. http://code.djangoproject.com/attachment/ticket/6362
"""
def clean(self, value):
if value is None:
value = u''
value = smart_str(value).strip()
value = super(forms.CharField, self).clean(value)
return value
def truncate_charfield(model):
"""decorator to truncate CharField data to model field max_length.
Apply to the clean method in views Form:
#truncate_charfield(MyModel)
def clean(self):
...
"""
def wrap(f):
def wrapped_f(*args):
f(*args)
d = args[0].cleaned_data
for field in model._meta.fields:
try:
mf = model._meta.get_field(field.name)
if isinstance(mf, models.CharField) and field.name in d:
d[field.name] = d[field.name][:mf.max_length]
except FieldDoesNotExist:
pass
return d
return wrapped_f
return wrap
If you're still not on Django 1.9+ shame on you (and me) and drop this into your form. This is similar to #jeremy-lewis's answer but I had several problems with his.
def clean_text_fields(self):
# TODO: Django 1.9, use on the model strip=True
# https://docs.djangoproject.com/en/1.9/ref/forms/fields/#charfield
from django.forms.fields import CharField
cd = self.cleaned_data
for field_name, field in self.fields.items():
if isinstance(field, CharField):
cd[field_name] = cd[field_name].strip()
if self.fields[field_name].required and not cd[field_name]:
self.add_error(field_name, "This is a required field.")
def clean(self):
self.clean_text_fields()

Dynamically setting the queryset of a ModelMultipleChoiceField to a custom recordset

I've seen all the howtos about how you can set a ModelMultipleChoiceField to use a custom queryset and I've tried them and they work. However, they all use the same paradigm: the queryset is just a filtered list of the same objects.
In my case, I'm trying to get the admin to draw a multiselect form that instead of using usernames as the text portion of the , I'd like to use the name field from my account class.
Here's a breakdown of what I've got:
# models.py
class Account(models.Model):
name = models.CharField(max_length=128,help_text="A display name that people understand")
user = models.ForeignKey(User, unique=True) # Tied to the User class in settings.py
class Organisation(models.Model):
administrators = models.ManyToManyField(User)
# admin.py
from django.forms import ModelMultipleChoiceField
from django.contrib.auth.models import User
class OrganisationAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
from ethico.accounts.models import Account
self.base_fields["administrators"] = ModelMultipleChoiceField(
queryset=User.objects.all(),
required=False
)
super(OrganisationAdminForm, self).__init__(*args, **kwargs)
class Meta:
model = Organisation
This works, however, I want queryset above to draw a selectbox with the Account.name property and the User.id property. This didn't work:
queryset=Account.objects.all().order_by("name").values_list("user","name")
It failed with this error:
'tuple' object has no attribute 'pk'
I figured that this would be easy, but it's turned into hours of dead-ends. Anyone care to shed some light?
You can use a custom widget, override its render method. Here's what I had done for a text field :
class UserToAccount(forms.widgets.TextInput):
def render(self, name, value, attrs=None):
if isinstance(value, User) :
value = Account.objects.get(user=value).name
return super (UserToAccount, self).render(name, value, attrs=None)
Then of course, use the widget parameter of your administrator field, in order to use your custom widget.
I don't know if it can be adapted for a select, but you can try out.
The queryset needs to be a QuerySet, when you do values_list you get a list so that won't work.
If you want to change the default display of models, just override __unicode__. See http://docs.djangoproject.com/en/dev/ref/models/instances/#unicode
For example:
def __unicode__(self):
return u"%s for %s" % (self.name, self.user)
Django will use __unicode__ whenever you asks it to print a model. For testing you can just load up a model in the shell and do print my_instance.
Taking a queue from sebpiq, I managed to figure it out:
class OrganisationAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
from django.forms import MultipleChoiceField
from ethico.accounts.models import Account
self.base_fields["administrators"] = MultipleChoiceField(
choices=tuple([(a.user_id, a.name) for a in Account.objects.all().order_by("name")]),
widget=forms.widgets.SelectMultiple,
required=False
)
super(OrganisationAdminForm, self).__init__(*args, **kwargs)
class Meta:
model = Organisation
class OrganisationAdmin(admin.ModelAdmin):
form = OrganisationAdminForm
admin.site.register(Organisation, OrganisationAdmin)
The key was abandoning the queryset altogether. Once I went with a fixed choices= parameter, everything just worked. Thanks everyone!

How to add custom fields to InlineFormsets?

I'm trying to add custom fields to an InlineFormset using the following code, but the fields won't show up in the Django Admin. Is the InlineFormset too locked down to allow this? My print "ding" test fires as expected, I can print out the form.fields and see them all there, but the actual fields are never rendered in the admin.
admin.py
from django.contrib import admin
import models
from django.forms.models import BaseInlineFormSet
from django import forms
from forms import ProgressForm
from django.template.defaultfilters import slugify
class ProgressInlineFormset(BaseInlineFormSet):
def add_fields(self, form, index):
print "ding"
super(ProgressInlineFormset, self).add_fields(form, index)
for criterion in models.Criterion.objects.all():
form.fields[slugify(criterion.name)] = forms.IntegerField(label=criterion.name)
class ProgressInline(admin.TabularInline):
model = models.Progress
extra = 8
formset = ProgressInlineFormset
class ReportAdmin(admin.ModelAdmin):
list_display = ("name", "pdf_column",)
search_fields = ["name",]
inlines = (ProgressInline,)
admin.site.register(models.Report, ReportAdmin)
I did it another way:
forms.py:
from django import forms
class ItemAddForm(forms.ModelForm):
my_new_field = forms.IntegerField(initial=1, label='quantity')
class Meta:
model = Item
admin.py:
from django.contrib import admin
from forms import *
class ItemAddInline(admin.TabularInline):
form = ItemAddForm
fields = (..., 'my_new_field')
This works so far, I only need to override somehow the save method to handle this new field. See this: http://docs.djangoproject.com/en/dev/ref/contrib/admin/#form . It says that by default Inlines use BaseModelForm, which is send to formset_factory. It doesn't work for me, tried to subclass BaseModelForm with errors (no attribute '_meta'). So I use ModelForm instead.
You can do it by another way (Dynamic forms):
admin.py
class ProgressInline(admin.TabularInline):
model = models.Progress
extra = 8
def get_formset(self, request, obj=None, **kwargs):
extra_fields = {'my_field': forms.CharField()}
kwargs['form'] = type('ProgressForm', (forms.ModelForm,), extra_fields)
return super(ProgressInline, self).get_formset(request, obj, **kwargs)
model = models.Progress
In the admin there will be only the fields defined in this Progress model. You have no fields/fieldsets option overwriting it.
If you want to add the new ones, there are two options:
In the model definition, add those new additional fields (make them optional!)
In the admin model (admin.TabularInline), add something something like:
fields = ('newfield1', 'newfield2', 'newfield3')
Take a look at fields, fieldsets.

Categories