Uri
Uri

Reputation: 3311

How do I specify the order of fields in a form? [django-crispy-forms]

We are using Django 2.1 for Speedy Net. We have a contact form and recently it has been used by spammers to send us spam. I decided to add a "no_bots" field to the form where I'm trying to prevent bots from submitting the form successfully. I checked the form and it works, but the problem is we have 2 sites - on one site (Speedy Net) the order of the fields is correct, and on the other site (Speedy Match) the order of the fields is not correct - the "no_bots" field comes before the "message" field but I want it to be the last field. How do I make it last? Our template tag contains just {% crispy form %} and I defined the order of the fields in class Meta:

class FeedbackForm(ModelFormWithDefaults):
    ...
    no_bots = forms.CharField(label=_('Type the number "17"'), required=True)

    class Meta:
        model = Feedback
        fields = ('sender_name', 'sender_email', 'text', 'no_bots')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelperWithDefaults()
        if (self.defaults.get('sender')):
            del self.fields['sender_name']
            del self.fields['sender_email']
            del self.fields['no_bots']
            self.helper.add_input(Submit('submit', pgettext_lazy(context=self.defaults['sender'].get_gender(), message='Send')))
        else:
            self.fields['sender_name'].required = True
            self.fields['sender_email'].required = True
            self.helper.add_layout(Row(
                Div('sender_name', css_class='col-md-6'),
                Div('sender_email', css_class='col-md-6'),
            ))
            self.helper.add_input(Submit('submit', _('Send')))

    def clean_text(self):
        text = self.cleaned_data.get('text')
        for not_allowed_string in self._not_allowed_strings:
            if (not_allowed_string in text):
                raise ValidationError(_("Please contact us by e-mail."))
        return text

    def clean_no_bots(self):
        no_bots = self.cleaned_data.get('no_bots')
        if (not (no_bots == "17")):
            raise ValidationError(_("Not 17."))
        return no_bots

Speedy Net Speedy Match

By the way, I checked our staging server, and there it's the opposite - the order of fields is correct in Speedy Match but not correct in Speedy Net. This is weird because they both use the same code! I think it means that the order of the fields is random.

Update: I deleted all the *.pyc files on the production server, and now the order of fields is correct in both sites. I also deleted these files on the staging server, and now the order of the fields is not correct in both sites. I did it again on the staging server and the order of the fields changed again in one of the sites.

Upvotes: 0

Views: 1268

Answers (2)

aaron
aaron

Reputation: 43138

The cause: iterating over an unordered collection

crispy_forms's FormHelper.render_layout does this:

fields = set(form.fields.keys())
left_fields_to_render = fields - form.rendered_fields
for field in left_fields_to_render:
    ...

At this point, left_fields_to_render is a set: {'text', 'no_bots'}

A set is an unordered collection.

This sometimes returns False: [a for a in {'text', 'no_bots'}] == ['text', 'no_bots']

You can try this by opening several different instances of the Python interpreter — I noticed that it's usually consistent within an instance of a Python interpreter.

The fix: iterate over a list

Basically:

fields = tuple(form.fields.keys())
left_fields_to_render = list_difference(fields, form.rendered_fields)
for field in left_fields_to_render:
    ...

I have submitted a PR for a more complete fix at django-crispy-forms/django-crispy-forms#952.

The workaround: explicitly specify the fields in the layout

Arguably, the intended usage of layout if you don't set render_unmentioned_fields = True.

You already call self.helper.add_layout for two of the four fields; you can just go all the way:

self.helper.add_layout(MultiWidgetField(
    Row(
        Div('sender_name', css_class='col-md-6'),
        Div('sender_email', css_class='col-md-6'),
    ),
    'text',
    'no_bots',
))

Upvotes: 2

tal.tzf
tal.tzf

Reputation: 128

Did you tried to use the Layout method?

From the documentation:

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit

class ExampleForm(forms.Form):
    [...]
    def __init__(self, *args, **kwargs):
        super(ExampleForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Fieldset(
                'first arg is the legend of the fieldset',
                'like_website',
                'favorite_number',
                'favorite_color',
                'favorite_food',
                'notes'
            ),
            ButtonHolder(
                Submit('submit', 'Submit', css_class='button white')
            )

https://django-crispy-forms.readthedocs.io/en/latest/layouts.html#fundamentals

Upvotes: 0

Related Questions