2

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.

Uri
  • 2,353
  • 8
  • 36
  • 76

3 Answers3

3

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.

Uri
  • 2,353
  • 8
  • 36
  • 76
2

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, '')
Dušan Maďar
  • 8,437
  • 4
  • 39
  • 62
  • Thanks for the idea. I defined `self.fields = OrderedDict(self.fields)` right after `super()...` in the `__init__` method (I don't think it should be in a for loop), and it works. But isn't there a problem with changing the definition of `self.fields`? – Uri Apr 10 '20 at 13:24
  • 1
    I don't think so as `OrderedDict` is a subclass of dict (https://docs.python.org/3.8/library/collections.html#collections.OrderedDict). – Dušan Maďar Apr 10 '20 at 13:27
  • 1
    I changed my mind and I can't accept this answer as the accepted answer. Please see my answer. – Uri Apr 11 '20 at 07:11
0

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

Ahmed I. Elsayed
  • 1,941
  • 2
  • 12
  • 30