Repository: django-crispy-forms/django-crispy-forms

Issue:
How do I specify the order of fields in a form?
* Package version: django-crispy-forms==1.7.2
* Django version: Django==2.1.14
* Python version: 3.6
* Template pack: CRISPY_TEMPLATE_PACK = 'bootstrap4'

### Description:

https://stackoverflow.com/questions/59101176/how-do-i-specify-the-order-of-fields-in-a-form-django-crispy-forms

We are using Django 2.1 for [Speedy Net](https://github.com/speedy-net/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`.

Our code:

```
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
```

Screenshots:

![Speedy Net](https://user-images.githubusercontent.com/12808124/69858174-20a26a80-129a-11ea-9fb7-5f648b6f168b.png)
![Speedy Match](https://user-images.githubusercontent.com/12808124/69858184-25ffb500-129a-11ea-8bbc-8f14b9af0193.png)



Discussion:
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.

Screenshots from staging:

![Speedy Match](https://user-images.githubusercontent.com/12808124/69858657-2187cc00-129b-11ea-9468-709496519fbc.png)
![Speedy Net](https://user-images.githubusercontent.com/12808124/69858661-25b3e980-129b-11ea-9e8d-b6ff8a5cbd2e.png)

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.

Resolving patch:
diff --git a/crispy_forms/helper.py b/crispy_forms/helper.py
--- a/crispy_forms/helper.py
+++ b/crispy_forms/helper.py
@@ -8,7 +8,7 @@
 from crispy_forms.layout import Layout
 from crispy_forms.layout_slice import LayoutSlice
 from crispy_forms.utils import (
-    TEMPLATE_PACK, flatatt, list_difference, list_intersection, render_field,
+    TEMPLATE_PACK, flatatt, list_difference, render_field,
 )
 
 try:
@@ -311,13 +311,13 @@ def render_layout(self, form, context, template_pack=TEMPLATE_PACK):
 
         # Rendering some extra fields if specified
         if self.render_unmentioned_fields or self.render_hidden_fields or self.render_required_fields:
-            fields = set(form.fields.keys())
-            left_fields_to_render = fields - form.rendered_fields
+            fields = tuple(form.fields.keys())
+            left_fields_to_render = list_difference(fields, form.rendered_fields)
             for field in left_fields_to_render:
                 if (
                     self.render_unmentioned_fields or
-                    self.render_hidden_fields and form.fields[field].widget.is_hidden or
-                    self.render_required_fields and form.fields[field].widget.is_required
+                    (self.render_hidden_fields and form.fields[field].widget.is_hidden) or
+                    (self.render_required_fields and form.fields[field].widget.is_required)
                 ):
                     html += render_field(
                         field,
@@ -327,28 +327,6 @@ def render_layout(self, form, context, template_pack=TEMPLATE_PACK):
                         template_pack=template_pack
                     )
 
-        # If the user has Meta.fields defined, not included in the layout,
-        # we suppose they need to be rendered
-        if hasattr(form, 'Meta'):
-            if hasattr(form.Meta, 'fields'):
-                current_fields = tuple(getattr(form, 'fields', {}).keys())
-                meta_fields = getattr(form.Meta, 'fields')
-
-                fields_to_render = list_intersection(current_fields, meta_fields)
-                left_fields_to_render = list_difference(fields_to_render, form.rendered_fields)
-
-                for field in left_fields_to_render:
-                    # We still respect the configuration of the helper
-                    # regarding which fields to render
-                    if (
-                        self.render_unmentioned_fields or
-                        (self.render_hidden_fields and
-                         form.fields[field].widget.is_hidden) or
-                        (self.render_required_fields and
-                         form.fields[field].widget.is_required)
-                    ):
-                        html += render_field(field, form, self.form_style, context)
-
         return mark_safe(html)
 
     def get_attributes(self, template_pack=TEMPLATE_PACK):


Test patch:
diff --git a/crispy_forms/tests/forms.py b/crispy_forms/tests/forms.py
--- a/crispy_forms/tests/forms.py
+++ b/crispy_forms/tests/forms.py
@@ -147,3 +147,21 @@ class Meta:
         model = CrispyEmptyChoiceTestModel
         fields = ['fruit']
         widgets = {'fruit': forms.RadioSelect() }
+
+
+class SampleForm7(forms.ModelForm):
+    is_company = forms.CharField(label="company", required=False, widget=forms.CheckboxInput())
+    password2 = forms.CharField(label="re-enter password", max_length=30, required=True, widget=forms.PasswordInput())
+
+    class Meta:
+        model = CrispyTestModel
+        fields = ('email', 'password', 'password2')
+
+
+class SampleForm8(forms.ModelForm):
+    is_company = forms.CharField(label="company", required=False, widget=forms.CheckboxInput())
+    password2 = forms.CharField(label="re-enter password", max_length=30, required=True, widget=forms.PasswordInput())
+
+    class Meta:
+        model = CrispyTestModel
+        fields = ('email', 'password2', 'password')
diff --git a/crispy_forms/tests/test_form_helper.py b/crispy_forms/tests/test_form_helper.py
--- a/crispy_forms/tests/test_form_helper.py
+++ b/crispy_forms/tests/test_form_helper.py
@@ -27,7 +27,13 @@
 from .conftest import (
     only_bootstrap, only_bootstrap3, only_bootstrap4, only_uni_form,
 )
-from .forms import SampleForm, SampleFormWithMedia, SampleFormWithMultiValueField
+from .forms import (
+    SampleForm,
+    SampleForm7,
+    SampleForm8,
+    SampleFormWithMedia,
+    SampleFormWithMultiValueField,
+)
 
 try:
     from django.middleware.csrf import _get_new_csrf_key
@@ -444,6 +450,58 @@ def test_disable_csrf():
     assert 'csrf' not in html
 
 
+def test_render_unmentioned_fields():
+    test_form = SampleForm()
+    test_form.helper = FormHelper()
+    test_form.helper.layout = Layout(
+        'email'
+    )
+    test_form.helper.render_unmentioned_fields = True
+
+    html = render_crispy_form(test_form)
+    assert html.count('<input') == 8
+
+
+def test_render_unmentioned_fields_order():
+    test_form = SampleForm7()
+    test_form.helper = FormHelper()
+    test_form.helper.layout = Layout(
+        'email'
+    )
+    test_form.helper.render_unmentioned_fields = True
+
+    html = render_crispy_form(test_form)
+    assert html.count('<input') == 4
+    assert (
+        # From layout
+        html.index('id="div_id_email"')
+        # From form.Meta.fields
+        < html.index('id="div_id_password"')
+        < html.index('id="div_id_password2"')
+        # From fields
+        < html.index('id="div_id_is_company"')
+    )
+
+    test_form = SampleForm8()
+    test_form.helper = FormHelper()
+    test_form.helper.layout = Layout(
+        'email'
+    )
+    test_form.helper.render_unmentioned_fields = True
+
+    html = render_crispy_form(test_form)
+    assert html.count('<input') == 4
+    assert (
+        # From layout
+        html.index('id="div_id_email"')
+        # From form.Meta.fields
+        < html.index('id="div_id_password2"')
+        < html.index('id="div_id_password"')
+        # From fields
+        < html.index('id="div_id_is_company"')
+    )
+
+
 def test_render_hidden_fields():
     from .utils import contains_partial
     test_form = SampleForm()