Reputation: 3311
We are using Django 2.2 and I want to upgrade to Django 3.0. We have a mixin (written in 2017) that add fields to forms:
class LocalizedFirstLastNameMixin(object):
def __init__(self, *args, **kwargs):
self.language_code = kwargs.pop('language_code', 'en')
super().__init__(*args, **kwargs)
for loc_field in reversed(self.get_localized_fields()):
self.fields[loc_field] = User._meta.get_field(loc_field).formfield()
self.fields[loc_field].required = True
self.fields.move_to_end(loc_field, last=False)
self.initial[loc_field] = getattr(self.instance, loc_field, '')
self.get_localized_fields()
returns ('first_name_en', 'last_name_en')
(in this order) in English, or the same localized to the current language we are using.
This mixin is used as one of the bases classes for forms which inherit from ModelForm
:
class RegistrationForm(AddAttributesToFieldsMixin, CleanEmailMixin, CleanNewPasswordMixin, CleanDateOfBirthMixin, LocalizedFirstLastNameMixin, forms.ModelForm):
....
class ProfileForm(AddAttributesToFieldsMixin, CleanDateOfBirthMixin, LocalizedFirstLastNameMixin, forms.ModelForm):
....
It works with Django versions up to 2.2. But when I upgrade to 3.0, I get this error message:
AttributeError: 'dict' object has no attribute 'move_to_end'
This function's info:
Move an existing element to the end (or beginning if last==False).
And it belongs to OrderedDict
.
So I guess we want these fields to be in the beginning of the form fields.
Is there a change in the implementation of the fields in forms in Django 3.0 and how do I specify the order of fields? And if I change it, will it work in previous versions such as Django 2.2?
I checked the Django 3.0 release notes and also releases from 3.0.1 to 3.0.5 and I didn't find any documentation of this issue.
Update: I found out that I can call self.order_fields(...)
, but how do I define the fields which come from the models? I only want to add two additional fields in the beginning of the list of fields.
Upvotes: 4
Views: 1946
Reputation: 8222
I'm late to this party, but this may be of use to somebody else. With recent Python (3.6+), an ordinary dict
keeps keys in the order they are added. With earlier versions, you need to use collections.OrderedDict
. Django used to use it's own dict-like class, but now uses a vanilla dict
. print( type(form_instance.fields))
if in doubt.
Anyway, you can re-order the fields any way you like by re-defining self.fields
in a form's __init__
method. As in:
class OrderUncWaferBatchForm( ModelForm):
#added non-model fields ...
ponum = forms.CharField( max_length=10,
label=mark_safe("Purchase Order<br>Number")
)
supplier = forms.CharField( max_length = 16)
def __init__(self, *args, **kwargs):
super().__init__( *args, **kwargs)
self.fields = reorder_dict( self.fields,
# these fields in this order at the top of the form
['ponum','supplier', 's_cost', 'wafer_stock_orig'])
class Meta:
model=WaferBatch
fields = ...
And since I've written a pretty general reorder_dict function, here it is
def reorder_dict(old_dict, reordering, dict_class=None):
""" Returns a new dict with keys added in the order specified by
reordering. This is a sequence of tuples (fromkey, tokey) which
operates on the list of keys of old_dict. Each tuple
(from_key, to_key) removes the named key from its current
posotion and inserts it before to_key. Each tuple operates
on the list permuted by all previous ones.
Any key that is not in old_dict will cause an IndexError
to_key can also be an integer for list.insert position. Using
a bignum will insert at the end. Using 0, at the top.
As a special case reordering can also be a list of key strings (not tuples).
This means to put the keys in the list at the top (positions 0,1,...) and
leave any keys not mentioned in theit original order after the
ones in the list.
Efficiency is not a consideration. It's intended for use
on form.fields, which rarely contains more than a handful of fields
"""
if dict_class is None:
dict_class = dict # on old Python wouold have to be an OrderedDict
if isinstance( reordering[0], str):
reordering = [ (k,n) for n,k in enumerate( reordering) ]
keys = list( old_dict.keys() )
for from_key, to_key in reordering:
fom = keys.index( from_key) # from is reserved word
if isinstance( to_key, int):
to = to_key
else:
to = keys.index( to_key)
keys.insert( to, from_key)
if to <= fom:
fom += 1 # it's been pushed down
del keys[fom]
result = dict_class()
for k in keys:
result[k] = old_dict[k]
return result
Upvotes: 0
Reputation: 3311
I asked in the Django developers mailing list and I have been told not to manipulate the order of fields myself, but instead to use the supported API methods documented here. So I changed the code and used self.order_fields
instead:
class LocalizedFirstLastNameMixin(object):
def __init__(self, *args, **kwargs):
self.language_code = kwargs.pop('language_code', 'en')
super().__init__(*args, **kwargs)
localized_fields = self.get_localized_fields()
for loc_field in localized_fields:
self.fields[loc_field] = User._meta.get_field(loc_field).formfield()
self.fields[loc_field].required = True
self.initial[loc_field] = getattr(self.instance, loc_field, '')
self.order_fields(field_order=localized_fields)
Notice that I only order the first two fields. All the other fields are kept in the default order. I also don't have to add the fields now in a reversed order.
Upvotes: 6
Reputation: 9919
Is there a change in the implementation of the fields in forms
What changed between Django 2.2 and 3 is how declared fields are initialized:
I guess this is because Django 3 supports Python from version 3.6 or later (https://docs.djangoproject.com/en/3.0/faq/install/) and since Python 3.6 dicts are insertion ordered (Are dictionaries ordered in Python 3.6+?).
I would convert self.fields
to an OrderedDict
(basically going back to what it used to be in version 2.2) to enable self.fields.move_to_end
again.
from collections import OrderedDict
class LocalizedFirstLastNameMixin(object):
def __init__(self, *args, **kwargs):
self.language_code = kwargs.pop('language_code', 'en')
super().__init__(*args, **kwargs)
self.fields = OrderedDict(self.fields)
for loc_field in reversed(self.get_localized_fields()):
self.fields[loc_field] = User._meta.get_field(loc_field).formfield()
self.fields[loc_field].required = True
self.fields.move_to_end(loc_field, last=False)
self.initial[loc_field] = getattr(self.instance, loc_field, '')
Upvotes: 2
Reputation: 2130
OrderedDict
is from the standard library.
from collections import OrderedDict
and instances have .move_to_end()
, It just says you dict
object has no .move_to_end()
which implies you're using a normal dict. Cast it to an OrderedDict
like this
x = { "key" : "value"}
y = OrderedDict(x)
# OrderedDict([('key', 'value')])
now .move_to_end()
will work
Upvotes: 0