Making a Django form class with a dynamic number of fields - python

I'm working on something like an online store. I'm making a form in which the customer buys an item, and she can choose how many of these item she would like to buy. But, on every item that she buys she needs to choose what its color would be. So there's a non-constant number of fields: If the customer buys 3 items, she should get 3 <select> boxes for choosing a color, if she buys 7 items, she should get 7 such <select> boxes.
I'll make the HTML form fields appear and disappear using JavaScript. But how do I deal with this on my Django form class? I see that form fields are class attributes, so I don't know how to deal with the fact that some form instance should have 3 color fields and some 7.
Any clue?

Jacob Kaplan-Moss has an extensive writeup on dynamic form fields:
http://jacobian.org/writing/dynamic-form-generation/
Essentially, you add more items to the form's self.fields dictionary during instantiation.

Here's another option: how about a formset?
Since your fields are all the same, that's precisely what formsets are used for.
The django admin uses FormSets + a bit of javascript to add arbitrary length inlines.
class ColorForm(forms.Form):
color = forms.ChoiceField(choices=(('blue', 'Blue'), ('red', 'Red')))
ColorFormSet = formset_factory(ColorForm, extra=0)
# we'll dynamically create the elements, no need for any forms
def myview(request):
if request.method == "POST":
formset = ColorFormSet(request.POST)
for form in formset.forms:
print "You've picked {0}".format(form.cleaned_data['color'])
else:
formset = ColorFormSet()
return render(request, 'template', {'formset': formset}))
JavaScript
<script>
$(function() {
// this is on click event just to demo.
// You would probably run this at page load or quantity change.
$("#generate_forms").click(function() {
// update total form count
quantity = $("[name=quantity]").val();
$("[name=form-TOTAL_FORMS]").val(quantity);
// copy the template and replace prefixes with the correct index
for (i=0;i<quantity;i++) {
// Note: Must use global replace here
html = $("#form_template").clone().html().replace(/__prefix_/g', i);
$("#forms").append(html);
};
})
})
</script>
Template
<form method="post">
{{ formset.management_form }}
<div style="display:none;" id="form_template">
{{ formset.empty_form.as_p }}
</div><!-- stores empty form for javascript -->
<div id="forms"></div><!-- where the generated forms go -->
</form>
<input type="text" name="quantity" value="6" />
<input type="submit" id="generate_forms" value="Generate Forms" />

you can do it like
def __init__(self, n, *args, **kwargs):
super(your_form, self).__init__(*args, **kwargs)
for i in range(0, n):
self.fields["field_name %d" % i] = forms.CharField()
and when you create form instance, you just do
forms = your_form(n)
it's just the basic idea, you can change the code to whatever your want. :D

The way I would do it is the following:
Create an "empty" class that inherits from froms.Form, like this:
class ItemsForm(forms.Form):
pass
Construct a dictionary of forms objects being the actual forms, whose composition would be dependent on the context (e.g. you can import them from an external module). For example:
new_fields = {
'milk' : forms.IntegerField(),
'butter': forms.IntegerField(),
'honey' : forms.IntegerField(),
'eggs' : forms.IntegerField()}
In views, you can use python native "type" function to dynamically generate a Form class with variable number of fields.
DynamicItemsForm = type('DynamicItemsForm', (ItemsForm,), new_fields)
Pass the content to the form and render it in the template:
Form = DynamicItemsForm(content)
context['my_form'] = Form
return render(request, "demo/dynamic.html", context)
The "content" is a dictionary of field values (e.g. even request.POST would do).
You can see my whole example explained here.

Another approach: Rather than breaking the normal field initialization flow, we can override fields with a mixin, return an OrderedDict of dynamic fields in generate_dynamic_fields which will be added whenever its set.
from collections import OrderedDict
class DynamicFormMixin:
_fields: OrderedDict = None
#property
def fields(self):
return self._fields
#fields.setter
def fields(self, value):
self._fields = value
self._fields.update(self.generate_dynamic_fields())
def generate_dynamic_fields(self):
return OrderedDict()
A simple example:
class ExampleForm(DynamicFormMixin, forms.Form):
instance = None
def __init__(self, instance = None, data=None, files=None, auto_id='id_%s', prefix=None, initial=None,
error_class=ErrorList, label_suffix=None, empty_permitted=False, field_order=None,
use_required_attribute=None, renderer=None):
self.instance = instance
super().__init__(data, files, auto_id, prefix, initial, error_class, label_suffix, empty_permitted, field_order,
use_required_attribute, renderer)
def generate_dynamic_fields(self):
dynamic_fields = OrderedDict()
instance = self.instance
dynamic_fields["dynamic_choices"] = forms.ChoiceField(label=_("Number of choices"),
choices=[(str(x), str(x)) for x in range(1, instance.number_of_choices + 1)],
initial=instance.initial_choice)
return dynamic_fields

Related

Django Form Dynamic Fields looping over each field from POST and creating records

I'm looking for some advice where to go from here. I've been working on making a Form, which dynamically generates its fields.
The form is working and generating everything correctly. However, I am having issues with how to save the actual form data. I'm looking for each field to save as a new item in a model.
The View Class from view.py
class MaintenanceCheckListForm(LoginRequiredMixin, FormView):
login_url = '/accounts/login'
template_name = 'maintenance/checklist.html'
form_class = MaintenanceCheckListForm
success_url = reverse_lazy('m-checklist')
def form_valid(self, form):
form.cleaned_data
for key, values in form:
MaintenanceCheckList.objects.create(
item = key,
is_compliant = values
)
return super().form_valid(form)
The Form from forms.py
class MaintenanceCheckListForm(forms.Form):
def __init__(self, *args, **kwargs):
super(MaintenanceCheckListForm, self).__init__(*args, **kwargs)
items = Maintenance_Item.objects.all()
CHOICES = (
('P','Compliant'),
('F','Non-Compliant'),
)
for item in items:
self.fields[str(item.name)] = forms.ChoiceField(
label=item.name,
choices=CHOICES,
widget=forms.RadioSelect,
initial='F',
)
The Model, from models.py
class MaintenanceCheckList(CommonInfo):
CHOICES = (
('P','Compliant'),
('F','Non-Compliant'),
)
id = models.AutoField(primary_key=True)
item = models.CharField(max_length=100)
is_compliant = models.CharField(max_length=20, choices= CHOICES)
I am having trouble accessing the data from the Form when it POST's. I've done some troubleshooting where I have set the values statically in the '''form_valid''' and it appears to generate the correct amounts of entires in the model. However the trouble begins when I attempt to insert the values from the POST.
I receieve the below error, which I believe it is trying to dump all the keys and values into a single item instead of looping over each key, value and creating the item.
DataError at /maintenance/checklist
value too long for type character varying(100)
Request Method: POST
Request URL: http://t1.localhost:8000/maintenance/checklist
Django Version: 3.1.6
Exception Type: DataError
Exception Value:
value too long for type character varying(100)
I'm fairly new to the world of Django (4 weeks and counting so far, and maybe 12 weeks into python). So any assistance would be amazing!
I believe you have somewhat gone on a tangent. There's a simpler solution of using Model formsets for what you want.
First if you want a custom form make that:
from django import forms
class MaintenanceCheckListComplianceForm(forms.ModelForm):
item = forms.CharField(widget = forms.HiddenInput())
is_compliant = forms.ChoiceField(
choices=MaintenanceCheckList.CHOICES,
widget=forms.RadioSelect,
initial='F',
)
class Meta:
model = MaintenanceCheckList
fields = ('item', 'is_compliant')
Next use it along with modelformset_factory in your views:
from django.forms import modelformset_factory
class MaintenanceCheckListFormView(LoginRequiredMixin, FormView): # Changed view name was a bit misleading
login_url = '/accounts/login'
template_name = 'maintenance/checklist.html'
success_url = reverse_lazy('m-checklist')
def form_valid(self, form):
instances = form.save()
return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['queryset'] = MaintenanceCheckList.objects.none()
kwargs['initial'] = [{'item': obj['name'], 'is_compliant': 'F'} for obj in Maintenance_Item.objects.all().values('name')]
return kwargs
def get_form(self, form_class=None):
kwargs = self.get_form_kwargs()
extra = len(kwargs['initial'])
form_class = modelformset_factory(MaintenanceCheckList, form=MaintenanceCheckListComplianceForm, extra=extra)
return form_class(**kwargs)
Now in your template:
<form method="post">
{{ form }}
</form>
Or manually render it:
<form method="post">
{{ form.management_form }}
{% for sub_form in form %}
Item: {{ sub_form.item.value }}
{{ sub_form }}
{% endfor %}
</form>
Note: The above usage is a bit weird due to the naming of the formset variable as form by the FormView you should look into improving that a bit.
Note: Looking at the implementation it feels a bit weird to do this. I would advice you to redesign your models a bit. Perhaps a foreign key between your models? It basically feels like you have duplicate data with this implementation.

how to show, set the value for input filed in django form using forms in html <input value={{ value}} disabled> how to do it in django forms

i'm taking values from database table in views file and has to render those values to a form in template file which is created by using the forms class and i have to show those values for some fields and make them immutable.
class OrderForm(forms.Form):
pid=forms.IntegerField()
pname=forms.CharField()
pprice=forms.FloatField()
person_name=forms.CharField(max_length=40)
emailid=forms.EmailField()
address=forms.CharField(max_length=40)
city=forms.CharField(max_length=20)
state=forms.CharField(max_length=20)
zip=forms.IntegerField()
card=forms.IntegerField()
exp= forms.DateField()
cvv=forms.IntegerField()
def order(request,pk):
pid=pk
user_name=request.user.username
qs=Product.objects.get(pid=pid)
pname=qs.pname.format()
list={'username':user_name,'pid':pid,'pname':pname}
form=OrderForm
return render(request,'order.html',{'data':list,'form':form})
i expect input filed with value that i passed by default which is immutable and when i submit i have to get same value i passed
Looks to me like you're better off using a ModelForm. It would be something like:
class OrderForm(forms.ModelForm)
class Meta:
model = Order
widgets = {
`immutable_field` : forms.TextInput(attrs={'readonly':True})
}
def order(request,pk):
pid=pk
user_name=request.user.username
qs=Product.objects.get(pid=pid)
pname=qs.pname.format()
list={'username':user_name,'pid':pid,'pname':pname}
form=OrderForm()
form.fields['immutable_field'] = "Some Value"
return render(request,'order.html',{'data':list,'form':form})
If you already have an order then you can prepopulate the fields with form=OrderForm(instance=order)
Make field as disable from Form init method and pass initial value from view section
class OrderForm(forms.Form):
pid=forms.IntegerField()
pname=forms.CharField()
pprice=forms.FloatField()
person_name=forms.CharField(max_length=40)
emailid=forms.EmailField()
address=forms.CharField(max_length=40)
city=forms.CharField(max_length=20)
state=forms.CharField(max_length=20)
zip=forms.IntegerField()
card=forms.IntegerField()
exp= forms.DateField()
cvv=forms.IntegerField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].disabled = True
self.fields['pid'].disabled = True
self.fields['pname'].disabled = True
Here in view you can pass dictionary to form as initial value of fields.
def order(request,pk):
pid=pk
user_name=request.user.username
qs=Product.objects.get(pid=pid)
pname=qs.pname.format()
initial={'username':user_name,'pid':pid,'pname':pname}
form=OrderForm(initial=initial)
return render(request,'order.html',{'data':initla,'form':form})

Exception on custom form field not being propagated and form reposting for use of tupple

I'm fairly new to python(3.5)/django(1.10), and I've run into the following problem:
I am using Django's generic CreateView to create a model and respective children model.
My objective is to save information about a purchase. A purchase is composed of a Bill and one or many Receipt. To accomplish this, I've created a custom Form (BillForm) with a custom field where user can input coma separated values that will be used to create receipts.
Here is what I have:
models.py
class Bill(models.Model):
""" A given bill whenever an item is purchased."""
number = models.CharField(_('Number'), max_length=20)
purchase_date = models.DateTimeField(_('Purchase Date'))
company = models.ForeignKey(Company, verbose_name=_('Company'),
on_delete=models.DO_NOTHING)
...
def _get_receipts(self):
''' returns all receipts that related to a bill '''
return Receipt.objects.filter(bill = self)
receipts = property(_get_receipts)
class Receipt(models.Model):
""" A receipt confirming a product is in our posession """
number = models.CharField(_('Number'), max_length=20)
bill = models.ForeignKey(Bill, verbose_name=_('Bill'),
on_delete=models.DO_NOTHING)
urls.py:
url(_(r'^create_purchase/$'), views.ObjectCreateView.as_view(
model=Bill,
form_class=BillForm
),
name="create_purchase"),
forms.py:
class MultipleReceiptsField(forms.Field):
''' A custom field to store a list of coma separated receipts '''
def to_python(self, value):
''' Normalize data to a set of receipts numbers '''
if not value:
return set()
return set(v.strip() for v in value.split(',') if v.strip())
def validate(self, value):
''' check if the values passed are less than 20 char long (limit for
model). '''
super(MultipleReceiptsField, self).validate(value)
# had made the following a tuple, then updated to list and worked
invalid_receipts = [r for r in value if len(r) > 20]
if invalid_receipts:
# EXCEPTION THROWN HERE BUT NOT PROPAGATED
raise ValidationError(
[ValidationError(_("Length of receipt %(r) is too large (max 20 characters)"),
code=INVALID_PARAMS, params={'r':int(r)}) # it was params={r:r} should have used 'r'
for r in invalid_receipts]
)
class BillForm(forms.ModelForm):
''' A form used to create a Purchase Bill. '''
receipts = MultipleReceiptsField(label=_("Receipts"), widget=forms.Textarea)
def __init__(self, *args, **kwargs):
''' constructor used to filter the companies. '''
# executes
super(BillForm, self).__init__(*args, **kwargs)
self.fields['company'].queryset =\ Company.objects.filter(is_provider=True).filter(is_active=True)
def save(self, commit=True):
''' createe receipts when saving a bll '''
# save logic | NEVER EXECUTES...
# for each elementt in receipts, create a receipt object and
# set its bill value to the bill that we just created
bill = super(BillForm, self).save(commit=False)
receipts_cd = self.cleaned_data["receipts"]
...
return bill
class Meta:
model = Bill
fields = ('company', "number", "currency", "price", "receipts")
view.py:
class ObjectCreateView(IsInPurchaseGroup, CreateView):
model = None
form_class = None
#fields = "__all__"
template_name = "purchases/object_form.html"
def get_context_data(self, **kwargs):
context = super(ObjectCreateView, self).get_context_data(**kwargs)
context["title"] = str(ACTIONS[CREATE_ACTION]) + " " +\
str(self.model._meta.verbose_name)
context["button"] = ACTIONS[CREATE_ACTION]
return context
def get_success_url(self):
next_url, kwargs = get_next_url(self.request)
if not next_url:
# works because the pluralization of both models end with s
next_url =\
"purchases:{}s".format((str(self.model).split(".")[-1])[:-2])
# if i am creating a bill, then i must navigate to
# create_purchased_products in the warehouse department.
if self.model == Bill:
next_url = "warehouse:create_purchased_products"
kwargs = {"receipts" : self.object.receipts}
return reverse_lazy(next_url, kwargs=kwargs)
object_form.html
{% extends "purchases/base.html" %}
{% load i18n %}
{% block body %}
<h3>{{ title }}</h3>
{% if error_message %}
<p><strong>{{ error_message }}</strong></p>
{% endif %}
<form action="" method="post">{% csrf_token %}
<table>{{ form.as_table }}</table>
<div class="buttons">
<input type="Submit" value="{{ button }}"/>
</div>
</form>
{% endblock %}
The problem:
whenever a form_class is given on the url, the following functions don't get executed:
BillForm.save()
ObjectCreateView.get_success_url()
If I omit the form_class on the url and set variable fields = "__all__" (simply creating an object of my Bill model), then the method ObjectCreateView.get_success_url() gets called.
My question
Why the BillForm.save() nor the ObjectCreateView.get_success_url() don't get executed? I know there has to be something wrong with the BillForm, but I can't seem to understand what...
Any help is greatly appreciated.
Update
Uploaded object_form.html and update forms.py
There is no exception being thrown. After clicking on Submit, a POST gets sent to the server. On the client (the browser), no changes happen (form remains with already entered data).
Found my errors; but I'm still unsure why it happens.
I had some exceptions being thrown in MultipleReceiptsField.validate(), but they didn't get propagated (when I submited the form, I didn't get exception, form just kept on posting).
After exception was fixed (saw that when placing invalid input will raise a validation error) and tried to submit with correct data, form kept on reposting.
Then I updated the invalid_receipts variable from being a tuple to a list and it began working
Am I missing some subtle difference between tuple and lists?
forms.py - not working:
class MultipleReceiptsField(forms.Field):
''' A custom field to store a list of coma separated receipts '''
def to_python(self, value):
''' Normalize data to a set of receipts numbers '''
if not value:
return set()
return set(v.strip() for v in value.split(',') if v.strip())
def validate(self, value):
''' check if the values passed are less than 20 char long (limit for
model). '''
super(MultipleReceiptsField, self).validate(value)
invalid_receipts = (r for r in value if len(r) > 20)
if invalid_receipts:
raise ValidationError(
[ValidationError(_("Length of recipt %(r) is too large (max 20 characters)"),
code=INVALID_PARAMS, params={'r':int(r)})
for r in invalid_receipts]
)
forms.py - working:
lass MultipleReceiptsField(forms.Field):
''' A custom field to store a list of coma separated receipts '''
def to_python(self, value):
''' Normalize data to a set of receipts numbers '''
if not value:
return set()
return set(v.strip() for v in value.split(',') if v.strip())
def validate(self, value):
''' check if the values passed are less than 20 char long (limit for
model). '''
super(MultipleReceiptsField, self).validate(value)
invalid_receipts = [r for r in value if len(r) > 20]
if invalid_receipts:
raise ValidationError(
[ValidationError(_("Length of receipt %(r) is too large (max 20 characters)"),
code=INVALID_PARAMS, params={'r':int(r)})
for r in invalid_receipts]
)
I think problem in method save. Method save return instance of object. If cant save it return None. Your method return None all time. Create view check result of form save after call other methods. If it see none poccess aborted. You need return some instance or call super.save like this:
def save(self, commit=True):
inst = super(BillForm, self).save(commit=False)
#addition logic, all what you need.
return inst
Or you need override valid method on your view class, without call super().valid() and override save method in form.
Sorry, my english very bad.

Key/value pairs in custom widget

So let's say I have jQuery convert a simple text input from this
<input class='employee_list' name='requestor' type='text' />
to this
<div name='requestor' class='ajax_picker'>
<input class='search_box' class='employee_list' name='requestor_text' type='text'/>
<input class='id' name='requestor' type='hidden' value='' />
<div class='results'></div>
</div>
And load the hidden input value with AJAX + JSON. Problem is, if form.is_valid() is not True, then how can I create a custom Widget that will render both values? Thinking I could do two different fields, but that seems ugly. I could also customize the form rendering, but that's even worse. Maybe the form can pass all of the POST data to the widget, but can't seem to figure out how to get that to work.
There must be an elegant way to achieve this!
from django import forms
class AjaxPickerWidget(forms.TextInput):
def render(self, name, value, attrs=None):
# ... now what?
return super(AjaxPickerWidget, self).render(name, value, attrs=attrs)
My Solution
Thanks for your help, I took a variant approach. Since I am using this strictly for Models (hence needing a key/value pair) I made the widget interact directly with the model and create a data-id attribute that the jQuery would catch to move to the hidden field.
from django import forms
class AjaxPickerModelWidget(forms.TextInput):
def __init__(self, css_class, queryset, id_name, value_name, attrs={}):
attrs['class'] = css_class
self.queryset = queryset
self.id_name = id_name
self.value_name = value_name
super(AjaxPickerModelWidget, self).__init__(attrs=attrs)
def render(self, name, value, attrs={}):
try:
instance = self.queryset.get(**{self.id_name: value})
attrs['data-id'] = value
value = getattr(instance, self.value_name)
except:
value = ''
return super(AjaxPickerModelWidget, self).render(name, value,
attrs=attrs)
Well, I don't know if this qualifies as elegant, but how about something like this. Here I'm storing the search string as a property on the widget, and setting the property from the POST data in the form constructor. I'm also using name="foo_picker_text" to indicate the search input for the corresponding input named "foo".
class AjaxPickerWidget(HiddenInput):
search_text_suffix = "_picker_text"
def __init__(self, **kwargs):
super(AjaxPickerWidget, self).__init__(**kwargs)
self.search_text = None
def render(self, name, value, attrs=None):
start = '<div name="%s" class="ajax_picker">' % (name) + \
'<input class="search_box" name="%s%s" ' % (name, self.search_text_suffix)+ \
'type="text" value="%s" />' % (self.search_text or "")
end = super(AjaxPickerWidget, self).render(name, value, attrs=attrs) + '</div>'
return '%s%s' % (start, end)
class MyForm(Form):
requestor = CharField(widget = AjaxPickerWidget())
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
# find picker text in POST data and tell the corresponding widget about it
for name in self.data:
if name.endswith(AjaxPickerWidget.search_text_suffix):
field_name = name.split(AjaxPickerWidget.search_text_suffix)[0]
self.fields[field_name].widget.search_text = self.data[name]
You might have to tweak it a bit to get attributes to show up where you want them, etc. but hopefully this basically does what you want?
You could instead of calling is_valid() on your form create a custom function
from django import forms
class AjaxPickerWidget(forms.TextInput):
def render(self, name, value, attrs=None):
return super(AjaxPickerWidget, self).render(name, value, attrs=attrs)
def validate(self):
values = self.cleaned_data.items():
# Modify the values to fit the original form (delete, change etc)
return whateversleft.is_valid() # Perhaps with some additional checks

Django forms, inheritance and order of form fields

I'm using Django forms in my website and would like to control the order of the fields.
Here's how I define my forms:
class edit_form(forms.Form):
summary = forms.CharField()
description = forms.CharField(widget=forms.TextArea)
class create_form(edit_form):
name = forms.CharField()
The name is immutable and should only be listed when the entity is created. I use inheritance to add consistency and DRY principles. What happens which is not erroneous, in fact totally expected, is that the name field is listed last in the view/html but I'd like the name field to be on top of summary and description. I do realize that I could easily fix it by copying summary and description into create_form and loose the inheritance but I'd like to know if this is possible.
Why? Imagine you've got 100 fields in edit_form and have to add 10 fields on the top in create_form - copying and maintaining the two forms wouldn't look so sexy then. (This is not my case, I'm just making up an example)
So, how can I override this behavior?
Edit:
Apparently there's no proper way to do this without going through nasty hacks (fiddling with .field attribute). The .field attribute is a SortedDict (one of Django's internal datastructures) which doesn't provide any way to reorder key:value pairs. It does how-ever provide a way to insert items at a given index but that would move the items from the class members and into the constructor. This method would work, but make the code less readable. The only other way I see fit is to modify the framework itself which is less-than-optimal in most situations.
In short the code would become something like this:
class edit_form(forms.Form):
summary = forms.CharField()
description = forms.CharField(widget=forms.TextArea)
class create_form(edit_form):
def __init__(self,*args,**kwargs):
forms.Form.__init__(self,*args,**kwargs)
self.fields.insert(0,'name',forms.CharField())
That shut me up :)
From Django 1.9+
Django 1.9 adds a new Form attribute, field_order, allowing to order the field regardless their order of declaration in the class.
class MyForm(forms.Form):
summary = forms.CharField()
description = forms.CharField(widget=forms.TextArea)
author = forms.CharField()
notes = form.CharField()
field_order = ['author', 'summary']
Missing fields in field_order keep their order in the class and are appended after the ones specified in the list. The example above will produce the fields in this order: ['author', 'summary', 'description', 'notes']
See the documentation: https://docs.djangoproject.com/en/stable/ref/forms/api/#notes-on-field-ordering
Up to Django 1.6
I had this same problem and I found another technique for reordering fields in the Django CookBook:
class EditForm(forms.Form):
summary = forms.CharField()
description = forms.CharField(widget=forms.TextArea)
class CreateForm(EditForm):
name = forms.CharField()
def __init__(self, *args, **kwargs):
super(CreateForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['name', 'summary', 'description']
From Django 1.9: https://docs.djangoproject.com/en/1.10/ref/forms/api/#notes-on-field-ordering
Original answer: Django 1.9 will support this by default on the form with field_order:
class MyForm(forms.Form):
...
field_order = ['field_1', 'field_2']
...
https://github.com/django/django/commit/28986da4ca167ae257abcaf7caea230eca2bcd80
I used the solution posted by Selene but found that it removed all fields which weren't assigned to keyOrder. The form that I'm subclassing has a lot of fields so this didn't work very well for me. I coded up this function to solve the problem using akaihola's answer, but if you want it to work like Selene's all you need to do is set throw_away to True.
def order_fields(form, field_list, throw_away=False):
"""
Accepts a form and a list of dictionary keys which map to the
form's fields. After running the form's fields list will begin
with the fields in field_list. If throw_away is set to true only
the fields in the field_list will remain in the form.
example use:
field_list = ['first_name', 'last_name']
order_fields(self, field_list)
"""
if throw_away:
form.fields.keyOrder = field_list
else:
for field in field_list[::-1]:
form.fields.insert(0, field, form.fields.pop(field))
This is how I'm using it in my own code:
class NestableCommentForm(ExtendedCommentSecurityForm):
# TODO: Have min and max length be determined through settings.
comment = forms.CharField(widget=forms.Textarea, max_length=100)
parent_id = forms.IntegerField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs):
super(NestableCommentForm, self).__init__(*args, **kwargs)
order_fields(self, ['comment', 'captcha'])
It appears that at some point the underlying structure of field order was changed from a django specific SordedDict to a python standard OrderedDict
Thus, in 1.7 I had to do the following:
from collections import OrderedDict
class MyForm(forms.Form):
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
original_fields = self.fields
new_order = OrderedDict()
for key in ['first', 'second', ... 'last']:
new_order[key] = original_fields[key]
self.fields = new_order
I'm sure someone could golf that into two or three lines, but for S.O. purposes I think clearly showing how it works is better than cleaver.
You could also create a decorator to order fields (inspired by Joshua's solution):
def order_fields(*field_list):
def decorator(form):
original_init = form.__init__
def init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
for field in field_list[::-1]:
self.fields.insert(0, field, self.fields.pop(field))
form.__init__ = init
return form
return decorator
This will ensure that all the fields passed to the decorator come first.
You can use it like this:
#order_fields('name')
class CreateForm(EditForm):
name = forms.CharField()
The accepted answer's approach makes use of an internal Django forms API that was changed in Django 1.7. The project team's opinion is that it should never have been used in the first place. I now use this function to reorder my forms. This code makes use of an OrderedDict:
def reorder_fields(fields, order):
"""Reorder form fields by order, removing items not in order.
>>> reorder_fields(
... OrderedDict([('a', 1), ('b', 2), ('c', 3)]),
... ['b', 'c', 'a'])
OrderedDict([('b', 2), ('c', 3), ('a', 1)])
"""
for key, v in fields.items():
if key not in order:
del fields[key]
return OrderedDict(sorted(fields.items(), key=lambda k: order.index(k[0])))
Which I use in classes like this:
class ChangeOpenQuestionForm(ChangeMultipleChoiceForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
key_order = ['title',
'question',
'answer',
'correct_answer',
'incorrect_answer']
self.fields = reorder_fields(self.fields, key_order)
For recent versions of Django (>=1.9), see the other answers' Form.field_order
See the notes in this SO question on the way Django's internals keep track of field order; the answers include suggestions on how to "reorder" fields to your liking (in the end it boils down to messing with the .fields attribute).
Alternate methods for changing the field order:
Pop-and-insert:
self.fields.insert(0, 'name', self.fields.pop('name'))
Pop-and-append:
self.fields['summary'] = self.fields.pop('summary')
self.fields['description'] = self.fields.pop('description')
Pop-and-append-all:
for key in ('name', 'summary', 'description'):
self.fields[key] = self.fields.pop(key)
Ordered-copy:
self.fields = SortedDict( [ (key, self.fields[key])
for key in ('name', 'summary' ,'description') ] )
But Selene's approach from the Django CookBook still feels clearest of all.
Based on an answer by #akaihola and updated to work with latest Django 1.5 as self.fields.insert is being depreciated.
from easycontactus.forms import *
from django import forms
class CustomEasyContactUsForm(EasyContactUsForm):
### form settings and configuration
CAPTHCA_PHRASE = 'igolf'
### native methods
def __init__(self, *args, **kwargs):
super(CustomEasyContactUsForm, self).__init__(*args, **kwargs)
# re-order placement of added attachment field
self.fields.keyOrder.insert(self.fields.keyOrder.index('captcha'),
self.fields.keyOrder.pop(self.fields.keyOrder.index('attachment'))
)
### field defintitions
attachment = forms.FileField()
In the above we are extending an EasyContactUsForm base class as it is defined in django-easycontactus package.
I built a form 'ExRegistrationForm' inherited from the 'RegistrationForm' from Django-Registration-Redux. I faced two issues, one of which was reordering the fields on the html output page once the new form had been created.
I solved them as follows:
1. ISSUE 1: Remove Username from the Registration Form: In my_app.forms.py
class ExRegistrationForm(RegistrationForm):
#Below 2 lines extend the RegistrationForm with 2 new fields firstname & lastname
first_name = forms.CharField(label=(u'First Name'))
last_name = forms.CharField(label=(u'Last Name'))
#Below 3 lines removes the username from the fields shown in the output of the this form
def __init__(self, *args, **kwargs):
super(ExRegistrationForm, self).__init__(*args, **kwargs)
self.fields.pop('username')
2. ISSUE 2: Make FirstName and LastName appear on top: In templates/registration/registration_form.html
You can individually display the fields in the order that you want. This would help in case the number of fields are less, but not if you have a large number of fields where it becomes practically impossible to actually write them in the form.
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<form method="post" action=".">
{% csrf_token %}
#The Default html is: {{ form.as_p }} , which can be broken down into individual elements as below for re-ordering.
<p>First Name: {{ form.first_name }}</p>
<p>Last Name: {{ form.last_name }}</p>
<p>Email: {{ form.email }}</p>
<p>Password: {{ form.password1 }}</p>
<p>Confirm Password: {{ form.password2 }}</p>
<input type="submit" value="{% trans 'Submit' %}" />
</form>
{% endblock %}
The above answers are right but incomplete. They only work if all the fields are defined as class variables. What about dynamic form fields which have to be defined in the intitialiser (__init__)?
from django import forms
class MyForm(forms.Form):
field1 = ...
field2 = ...
field_order = ['val', 'field1', 'field2']
def __init__(self, val_list, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
vals = zip(val_list, val_list)
self.fields['val'] = forms.CharField(choices=vals)
The above will never work for val but will work for field1 and field2 (if we reorder them). You might want to try defining field_order in the initialiser:
class MyForm(forms.Form):
# other fields
def __init__(self, val_list, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
vals = zip(val_list, val_list)
self.fields['val'] = forms.CharField(choices=vals)
self.field_order = ['val', 'field1', 'field2']
but this will also fail because the field order is fixed before the call to super().
Therefore the only solution is the constructor (__new__) and set field_order to a class variable.
class MyForm(forms.Form):
# other fields
field_order = ['val', 'field1', 'field2']
def __new__(cls, val_list, *args, **kwargs):
form = super(MyForm, cls).__new__(cls)
vals = zip(val_list, val_list)
form.base_fields['val'] = forms.CharField(choices=vals)
return form

Categories