We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Memberships: prevent changing consortium to non-consortium if there are >1 members assigned
Current implementation allows for membership to be changed from consortium (multiple members) to non-consortium (only one member), but a validation should be implemented to prevent that change when more than one member exists.

@maneesha may provide additional logic for the change, for example _the remaining member should receive a different membership role_.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `amy/fiscal/forms.py`
Content:
```
1 from crispy_forms.layout import Div, HTML, Field
2 from django import forms
3 from django.core.exceptions import ValidationError
4 from django.core.validators import RegexValidator
5 from django.dispatch import receiver
6 from django.urls import reverse
7 from markdownx.fields import MarkdownxFormField
8 
9 from fiscal.models import MembershipTask
10 from workshops.forms import (
11     BootstrapHelper,
12     form_saved_add_comment,
13 )
14 from workshops.models import (
15     Organization,
16     Member,
17     Membership,
18 )
19 
20 # this is used instead of Django Autocomplete Light widgets
21 # see issue #1330: https://github.com/swcarpentry/amy/issues/1330
22 from workshops.fields import (
23     ModelSelect2Widget,
24 )
25 from workshops.signals import create_comment_signal
26 
27 
28 # settings for Select2
29 # this makes it possible for autocomplete widget to fit in low-width sidebar
30 SIDEBAR_DAL_WIDTH = {
31     "data-width": "100%",
32     "width": "style",
33 }
34 
35 
36 class OrganizationForm(forms.ModelForm):
37     domain = forms.CharField(
38         max_length=Organization._meta.get_field("domain").max_length,
39         validators=[
40             RegexValidator(
41                 r"[^\w\.-]+",
42                 inverse_match=True,
43                 message='Please enter only the domain (such as "math.esu.edu")'
44                 ' without a leading "http://" or a trailing "/".',
45             )
46         ],
47     )
48 
49     helper = BootstrapHelper(add_cancel_button=False, duplicate_buttons_on_top=True)
50 
51     class Meta:
52         model = Organization
53         fields = ["domain", "fullname", "country", "latitude", "longitude"]
54 
55 
56 class OrganizationCreateForm(OrganizationForm):
57     comment = MarkdownxFormField(
58         label="Comment",
59         help_text="This will be added to comments after the organization "
60         "is created.",
61         widget=forms.Textarea,
62         required=False,
63     )
64 
65     class Meta(OrganizationForm.Meta):
66         fields = OrganizationForm.Meta.fields.copy()
67         fields.append("comment")
68 
69     def save(self, *args, **kwargs):
70         res = super().save(*args, **kwargs)
71 
72         create_comment_signal.send(
73             sender=self.__class__,
74             content_object=res,
75             comment=self.cleaned_data["comment"],
76             timestamp=None,
77         )
78 
79         return res
80 
81 
82 class MembershipForm(forms.ModelForm):
83     helper = BootstrapHelper(add_cancel_button=False)
84 
85     class Meta:
86         model = Membership
87         fields = [
88             "name",
89             "consortium",
90             "public_status",
91             "variant",
92             "agreement_start",
93             "agreement_end",
94             "contribution_type",
95             "registration_code",
96             "agreement_link",
97             "workshops_without_admin_fee_per_agreement",
98             "public_instructor_training_seats",
99             "additional_public_instructor_training_seats",
100             "inhouse_instructor_training_seats",
101             "additional_inhouse_instructor_training_seats",
102             "emergency_contact",
103         ]
104 
105     def __init__(self, *args, **kwargs):
106         super().__init__(*args, **kwargs)
107 
108         # set up a layout object for the helper
109         self.helper.layout = self.helper.build_default_layout(self)
110 
111         # add warning alert for dates falling within next 2-3 months
112         INVALID_AGREEMENT_DURATION_WARNING = (
113             "The selected agreement dates fall out of the typical 1-year long duration."
114         )
115         pos_index = self.helper.layout.fields.index("agreement_end")
116         self.helper.layout.insert(
117             pos_index + 1,
118             Div(
119                 Div(
120                     HTML(INVALID_AGREEMENT_DURATION_WARNING),
121                     css_class="alert alert-warning offset-lg-2 col-lg-8 col-12",
122                 ),
123                 id="agreement_duration_warning",
124                 css_class="form-group row d-none",
125             ),
126         )
127 
128     def clean(self):
129         super().clean()
130         errors = dict()
131 
132         # validate agreement end date is no sooner than start date
133         agreement_start = self.cleaned_data.get("agreement_start")
134         agreement_end = self.cleaned_data.get("agreement_end")
135         try:
136             if agreement_end < agreement_start:
137                 errors["agreement_end"] = ValidationError(
138                     "Agreement end date can't be sooner than the start date."
139                 )
140         except TypeError:
141             pass
142 
143         if errors:
144             raise ValidationError(errors)
145 
146 
147 class MembershipCreateForm(MembershipForm):
148     comment = MarkdownxFormField(
149         label="Comment",
150         help_text="This will be added to comments after the membership is created.",
151         widget=forms.Textarea,
152         required=False,
153     )
154 
155     helper = BootstrapHelper(add_cancel_button=True)
156 
157     main_organization = forms.ModelChoiceField(
158         Organization.objects.all(),
159         label="Main organisation",
160         required=True,
161         widget=ModelSelect2Widget(data_view="organization-lookup"),
162         help_text="Select main organisation (e.g. Signatory in case of consortium).",
163     )
164 
165     class Meta(MembershipForm.Meta):
166         fields = MembershipForm.Meta.fields.copy()
167         fields.insert(0, "main_organization")
168         fields.append("comment")
169 
170     class Media:
171         js = ("membership_create.js",)
172 
173     def __init__(self, *args, **kwargs):
174         super().__init__(*args, **kwargs)
175 
176         self.fields["consortium"].help_text += (
177             "<br>If you select this option, you'll be taken to the next screen to "
178             "select organisations engaged in consortium. You must create the "
179             "organisation (<a href='{}'>here</a>) before applying it to this "
180             "membership."
181         ).format(reverse("organization_add"))
182 
183     def save(self, *args, **kwargs):
184         res = super().save(*args, **kwargs)
185 
186         create_comment_signal.send(
187             sender=self.__class__,
188             content_object=res,
189             comment=self.cleaned_data["comment"],
190             timestamp=None,
191         )
192 
193         return res
194 
195 
196 class MembershipRollOverForm(MembershipCreateForm):
197     main_organization = None  # remove the additional field
198 
199     class Meta(MembershipCreateForm.Meta):
200         fields = [
201             "name",
202             "consortium",
203             "public_status",
204             "variant",
205             "agreement_start",
206             "agreement_end",
207             "contribution_type",
208             "registration_code",
209             "agreement_link",
210             "workshops_without_admin_fee_per_agreement",
211             "workshops_without_admin_fee_rolled_from_previous",
212             "public_instructor_training_seats",
213             "additional_public_instructor_training_seats",
214             "public_instructor_training_seats_rolled_from_previous",
215             "inhouse_instructor_training_seats",
216             "additional_inhouse_instructor_training_seats",
217             "inhouse_instructor_training_seats_rolled_from_previous",
218             "emergency_contact",
219             "comment",
220         ]
221 
222     def __init__(self, *args, **kwargs):
223         super().__init__(*args, **kwargs)
224         self["workshops_without_admin_fee_rolled_from_previous"].field.disabled = True
225         self[
226             "public_instructor_training_seats_rolled_from_previous"
227         ].field.disabled = True
228         self[
229             "inhouse_instructor_training_seats_rolled_from_previous"
230         ].field.disabled = True
231 
232 
233 class MemberForm(forms.ModelForm):
234     """Form intended to use in formset for creating multiple membership members."""
235 
236     helper = BootstrapHelper(
237         add_cancel_button=False, form_tag=False, add_submit_button=False
238     )
239     helper_empty_form = BootstrapHelper(
240         add_cancel_button=False, form_tag=False, add_submit_button=False
241     )
242 
243     class Meta:
244         model = Member
245         fields = [
246             "organization",
247             "role",
248         ]
249         widgets = {
250             "organization": ModelSelect2Widget(data_view="organization-lookup"),
251             "role": ModelSelect2Widget(data_view="memberrole-lookup"),
252         }
253 
254     def __init__(self, *args, **kwargs):
255         super().__init__(*args, **kwargs)
256 
257         # set up layout objects for the helpers - they're identical except for
258         # visibility of the delete checkbox
259         self.helper.layout = self.helper.build_default_layout(self)
260         self.helper_empty_form.layout = self.helper.build_default_layout(self)
261         self.helper.layout.append(Field("id"))
262         self.helper.layout.append(Field("DELETE"))  # visible; formset adds it
263         self.helper_empty_form.layout.append(Field("id"))
264         self.helper_empty_form.layout.append(
265             Div(Field("DELETE"), css_class="d-none")  # hidden
266         )
267 
268 
269 class MembershipTaskForm(forms.ModelForm):
270     """Form intended to use in formset for creating multiple membership members."""
271 
272     helper = BootstrapHelper(
273         add_cancel_button=False, form_tag=False, add_submit_button=False
274     )
275     helper_empty_form = BootstrapHelper(
276         add_cancel_button=False, form_tag=False, add_submit_button=False
277     )
278 
279     class Meta:
280         model = MembershipTask
281         fields = [
282             "person",
283             "role",
284         ]
285         widgets = {
286             "person": ModelSelect2Widget(data_view="person-lookup"),
287             "role": ModelSelect2Widget(data_view="membershippersonrole-lookup"),
288         }
289 
290     def __init__(self, *args, **kwargs):
291         super().__init__(*args, **kwargs)
292 
293         # set up layout objects for the helpers - they're identical except for
294         # visibility of the delete checkbox
295         self.helper.layout = self.helper.build_default_layout(self)
296         self.helper_empty_form.layout = self.helper.build_default_layout(self)
297         self.helper.layout.append(Field("id"))
298         self.helper.layout.append(Field("DELETE"))  # visible; formset adds it
299         self.helper_empty_form.layout.append(Field("id"))
300         self.helper_empty_form.layout.append(
301             Div(Field("DELETE"), css_class="d-none")  # hidden
302         )
303 
304 
305 class MembershipExtensionForm(forms.Form):
306     agreement_start = forms.DateField(disabled=True, required=False)
307     agreement_end = forms.DateField(disabled=True, required=False)
308     extension = forms.IntegerField(
309         min_value=1,
310         required=True,
311         help_text="Number of days the agreement should be extended.",
312     )
313     new_agreement_end = forms.DateField(disabled=True, required=False)
314 
315     helper = BootstrapHelper()
316 
317     class Media:
318         js = ("membership_extend.js", "date_yyyymmdd.js")
319 
320 
321 # ----------------------------------------------------------
322 # Signals
323 
324 # adding @receiver decorator to the function defined in `workshops.forms`
325 form_saved_add_comment = receiver(
326     create_comment_signal,
327     sender=OrganizationCreateForm,
328 )(form_saved_add_comment)
329 
330 # adding @receiver decorator to the function defined in `workshops.forms`
331 form_saved_add_comment = receiver(
332     create_comment_signal,
333     sender=MembershipCreateForm,
334 )(form_saved_add_comment)
335
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/amy/fiscal/forms.py b/amy/fiscal/forms.py
--- a/amy/fiscal/forms.py
+++ b/amy/fiscal/forms.py
@@ -1,4 +1,4 @@
-from crispy_forms.layout import Div, HTML, Field
+from crispy_forms.layout import Div, HTML
 from django import forms
 from django.core.exceptions import ValidationError
 from django.core.validators import RegexValidator
@@ -140,6 +140,16 @@
         except TypeError:
             pass
 
+        # check if multiple members are assigned - then disallow changing to
+        # non-consortium
+        new_consortium = self.cleaned_data.get("consortium")
+        members_count = self.instance.member_set.count()
+        if not new_consortium and members_count > 1:
+            errors["consortium"] = ValidationError(
+                "Cannot change to non-consortium when there are multiple members "
+                "assigned. Remove the members so that at most 1 is left."
+            )
+
         if errors:
             raise ValidationError(errors)
 
@@ -230,7 +240,20 @@
         ].field.disabled = True
 
 
-class MemberForm(forms.ModelForm):
+class EditableFormsetFormMixin(forms.ModelForm):
+    EDITABLE = forms.BooleanField(
+        label="Change",
+        required=False,
+        widget=forms.CheckboxInput(attrs={"data-form-editable-check": ""}),
+    )
+
+    def clean(self):
+        if self.has_changed() and not self.cleaned_data["EDITABLE"]:
+            raise ValidationError("Form values weren't supposed to be changed.")
+        return super().clean()
+
+
+class MemberForm(EditableFormsetFormMixin, forms.ModelForm):
     """Form intended to use in formset for creating multiple membership members."""
 
     helper = BootstrapHelper(
@@ -257,16 +280,19 @@
         # set up layout objects for the helpers - they're identical except for
         # visibility of the delete checkbox
         self.helper.layout = self.helper.build_default_layout(self)
+        self.helper.layout.append("id")
+        self.helper.layout.append("DELETE")  # visible; formset adds it
         self.helper_empty_form.layout = self.helper.build_default_layout(self)
-        self.helper.layout.append(Field("id"))
-        self.helper.layout.append(Field("DELETE"))  # visible; formset adds it
-        self.helper_empty_form.layout.append(Field("id"))
+        self.helper_empty_form.layout.append("id")
         self.helper_empty_form.layout.append(
-            Div(Field("DELETE"), css_class="d-none")  # hidden
+            Div("DELETE", css_class="d-none")  # hidden
         )
+        # remove EDITABLE checkbox from empty helper form
+        pos_index = self.helper_empty_form.layout.fields.index("EDITABLE")
+        self.helper_empty_form.layout.pop(pos_index)
 
 
-class MembershipTaskForm(forms.ModelForm):
+class MembershipTaskForm(EditableFormsetFormMixin, forms.ModelForm):
     """Form intended to use in formset for creating multiple membership members."""
 
     helper = BootstrapHelper(
@@ -293,13 +319,16 @@
         # set up layout objects for the helpers - they're identical except for
         # visibility of the delete checkbox
         self.helper.layout = self.helper.build_default_layout(self)
+        self.helper.layout.append("id")
+        self.helper.layout.append("DELETE")  # visible; formset adds it
         self.helper_empty_form.layout = self.helper.build_default_layout(self)
-        self.helper.layout.append(Field("id"))
-        self.helper.layout.append(Field("DELETE"))  # visible; formset adds it
-        self.helper_empty_form.layout.append(Field("id"))
+        self.helper_empty_form.layout.append("id")
         self.helper_empty_form.layout.append(
-            Div(Field("DELETE"), css_class="d-none")  # hidden
+            Div("DELETE", css_class="d-none")  # hidden
         )
+        # remove EDITABLE checkbox from empty helper form
+        pos_index = self.helper_empty_form.layout.fields.index("EDITABLE")
+        self.helper_empty_form.layout.pop(pos_index)
 
 
 class MembershipExtensionForm(forms.Form):

{"golden_diff": "diff --git a/amy/fiscal/forms.py b/amy/fiscal/forms.py\n--- a/amy/fiscal/forms.py\n+++ b/amy/fiscal/forms.py\n@@ -1,4 +1,4 @@\n-from crispy_forms.layout import Div, HTML, Field\n+from crispy_forms.layout import Div, HTML\n from django import forms\n from django.core.exceptions import ValidationError\n from django.core.validators import RegexValidator\n@@ -140,6 +140,16 @@\n         except TypeError:\n             pass\n \n+        # check if multiple members are assigned - then disallow changing to\n+        # non-consortium\n+        new_consortium = self.cleaned_data.get(\"consortium\")\n+        members_count = self.instance.member_set.count()\n+        if not new_consortium and members_count > 1:\n+            errors[\"consortium\"] = ValidationError(\n+                \"Cannot change to non-consortium when there are multiple members \"\n+                \"assigned. Remove the members so that at most 1 is left.\"\n+            )\n+\n         if errors:\n             raise ValidationError(errors)\n \n@@ -230,7 +240,20 @@\n         ].field.disabled = True\n \n \n-class MemberForm(forms.ModelForm):\n+class EditableFormsetFormMixin(forms.ModelForm):\n+    EDITABLE = forms.BooleanField(\n+        label=\"Change\",\n+        required=False,\n+        widget=forms.CheckboxInput(attrs={\"data-form-editable-check\": \"\"}),\n+    )\n+\n+    def clean(self):\n+        if self.has_changed() and not self.cleaned_data[\"EDITABLE\"]:\n+            raise ValidationError(\"Form values weren't supposed to be changed.\")\n+        return super().clean()\n+\n+\n+class MemberForm(EditableFormsetFormMixin, forms.ModelForm):\n     \"\"\"Form intended to use in formset for creating multiple membership members.\"\"\"\n \n     helper = BootstrapHelper(\n@@ -257,16 +280,19 @@\n         # set up layout objects for the helpers - they're identical except for\n         # visibility of the delete checkbox\n         self.helper.layout = self.helper.build_default_layout(self)\n+        self.helper.layout.append(\"id\")\n+        self.helper.layout.append(\"DELETE\")  # visible; formset adds it\n         self.helper_empty_form.layout = self.helper.build_default_layout(self)\n-        self.helper.layout.append(Field(\"id\"))\n-        self.helper.layout.append(Field(\"DELETE\"))  # visible; formset adds it\n-        self.helper_empty_form.layout.append(Field(\"id\"))\n+        self.helper_empty_form.layout.append(\"id\")\n         self.helper_empty_form.layout.append(\n-            Div(Field(\"DELETE\"), css_class=\"d-none\")  # hidden\n+            Div(\"DELETE\", css_class=\"d-none\")  # hidden\n         )\n+        # remove EDITABLE checkbox from empty helper form\n+        pos_index = self.helper_empty_form.layout.fields.index(\"EDITABLE\")\n+        self.helper_empty_form.layout.pop(pos_index)\n \n \n-class MembershipTaskForm(forms.ModelForm):\n+class MembershipTaskForm(EditableFormsetFormMixin, forms.ModelForm):\n     \"\"\"Form intended to use in formset for creating multiple membership members.\"\"\"\n \n     helper = BootstrapHelper(\n@@ -293,13 +319,16 @@\n         # set up layout objects for the helpers - they're identical except for\n         # visibility of the delete checkbox\n         self.helper.layout = self.helper.build_default_layout(self)\n+        self.helper.layout.append(\"id\")\n+        self.helper.layout.append(\"DELETE\")  # visible; formset adds it\n         self.helper_empty_form.layout = self.helper.build_default_layout(self)\n-        self.helper.layout.append(Field(\"id\"))\n-        self.helper.layout.append(Field(\"DELETE\"))  # visible; formset adds it\n-        self.helper_empty_form.layout.append(Field(\"id\"))\n+        self.helper_empty_form.layout.append(\"id\")\n         self.helper_empty_form.layout.append(\n-            Div(Field(\"DELETE\"), css_class=\"d-none\")  # hidden\n+            Div(\"DELETE\", css_class=\"d-none\")  # hidden\n         )\n+        # remove EDITABLE checkbox from empty helper form\n+        pos_index = self.helper_empty_form.layout.fields.index(\"EDITABLE\")\n+        self.helper_empty_form.layout.pop(pos_index)\n \n \n class MembershipExtensionForm(forms.Form):\n", "issue": "Memberships: prevent changing consortium to non-consortium if there are >1 members assigned\nCurrent implementation allows for membership to be changed from consortium (multiple members) to non-consortium (only one member), but a validation should be implemented to prevent that change when more than one member exists.\r\n\r\n@maneesha may provide additional logic for the change, for example _the remaining member should receive a different membership role_.\n", "before_files": [{"content": "from crispy_forms.layout import Div, HTML, Field\nfrom django import forms\nfrom django.core.exceptions import ValidationError\nfrom django.core.validators import RegexValidator\nfrom django.dispatch import receiver\nfrom django.urls import reverse\nfrom markdownx.fields import MarkdownxFormField\n\nfrom fiscal.models import MembershipTask\nfrom workshops.forms import (\n    BootstrapHelper,\n    form_saved_add_comment,\n)\nfrom workshops.models import (\n    Organization,\n    Member,\n    Membership,\n)\n\n# this is used instead of Django Autocomplete Light widgets\n# see issue #1330: https://github.com/swcarpentry/amy/issues/1330\nfrom workshops.fields import (\n    ModelSelect2Widget,\n)\nfrom workshops.signals import create_comment_signal\n\n\n# settings for Select2\n# this makes it possible for autocomplete widget to fit in low-width sidebar\nSIDEBAR_DAL_WIDTH = {\n    \"data-width\": \"100%\",\n    \"width\": \"style\",\n}\n\n\nclass OrganizationForm(forms.ModelForm):\n    domain = forms.CharField(\n        max_length=Organization._meta.get_field(\"domain\").max_length,\n        validators=[\n            RegexValidator(\n                r\"[^\\w\\.-]+\",\n                inverse_match=True,\n                message='Please enter only the domain (such as \"math.esu.edu\")'\n                ' without a leading \"http://\" or a trailing \"/\".',\n            )\n        ],\n    )\n\n    helper = BootstrapHelper(add_cancel_button=False, duplicate_buttons_on_top=True)\n\n    class Meta:\n        model = Organization\n        fields = [\"domain\", \"fullname\", \"country\", \"latitude\", \"longitude\"]\n\n\nclass OrganizationCreateForm(OrganizationForm):\n    comment = MarkdownxFormField(\n        label=\"Comment\",\n        help_text=\"This will be added to comments after the organization \"\n        \"is created.\",\n        widget=forms.Textarea,\n        required=False,\n    )\n\n    class Meta(OrganizationForm.Meta):\n        fields = OrganizationForm.Meta.fields.copy()\n        fields.append(\"comment\")\n\n    def save(self, *args, **kwargs):\n        res = super().save(*args, **kwargs)\n\n        create_comment_signal.send(\n            sender=self.__class__,\n            content_object=res,\n            comment=self.cleaned_data[\"comment\"],\n            timestamp=None,\n        )\n\n        return res\n\n\nclass MembershipForm(forms.ModelForm):\n    helper = BootstrapHelper(add_cancel_button=False)\n\n    class Meta:\n        model = Membership\n        fields = [\n            \"name\",\n            \"consortium\",\n            \"public_status\",\n            \"variant\",\n            \"agreement_start\",\n            \"agreement_end\",\n            \"contribution_type\",\n            \"registration_code\",\n            \"agreement_link\",\n            \"workshops_without_admin_fee_per_agreement\",\n            \"public_instructor_training_seats\",\n            \"additional_public_instructor_training_seats\",\n            \"inhouse_instructor_training_seats\",\n            \"additional_inhouse_instructor_training_seats\",\n            \"emergency_contact\",\n        ]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # set up a layout object for the helper\n        self.helper.layout = self.helper.build_default_layout(self)\n\n        # add warning alert for dates falling within next 2-3 months\n        INVALID_AGREEMENT_DURATION_WARNING = (\n            \"The selected agreement dates fall out of the typical 1-year long duration.\"\n        )\n        pos_index = self.helper.layout.fields.index(\"agreement_end\")\n        self.helper.layout.insert(\n            pos_index + 1,\n            Div(\n                Div(\n                    HTML(INVALID_AGREEMENT_DURATION_WARNING),\n                    css_class=\"alert alert-warning offset-lg-2 col-lg-8 col-12\",\n                ),\n                id=\"agreement_duration_warning\",\n                css_class=\"form-group row d-none\",\n            ),\n        )\n\n    def clean(self):\n        super().clean()\n        errors = dict()\n\n        # validate agreement end date is no sooner than start date\n        agreement_start = self.cleaned_data.get(\"agreement_start\")\n        agreement_end = self.cleaned_data.get(\"agreement_end\")\n        try:\n            if agreement_end < agreement_start:\n                errors[\"agreement_end\"] = ValidationError(\n                    \"Agreement end date can't be sooner than the start date.\"\n                )\n        except TypeError:\n            pass\n\n        if errors:\n            raise ValidationError(errors)\n\n\nclass MembershipCreateForm(MembershipForm):\n    comment = MarkdownxFormField(\n        label=\"Comment\",\n        help_text=\"This will be added to comments after the membership is created.\",\n        widget=forms.Textarea,\n        required=False,\n    )\n\n    helper = BootstrapHelper(add_cancel_button=True)\n\n    main_organization = forms.ModelChoiceField(\n        Organization.objects.all(),\n        label=\"Main organisation\",\n        required=True,\n        widget=ModelSelect2Widget(data_view=\"organization-lookup\"),\n        help_text=\"Select main organisation (e.g. Signatory in case of consortium).\",\n    )\n\n    class Meta(MembershipForm.Meta):\n        fields = MembershipForm.Meta.fields.copy()\n        fields.insert(0, \"main_organization\")\n        fields.append(\"comment\")\n\n    class Media:\n        js = (\"membership_create.js\",)\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.fields[\"consortium\"].help_text += (\n            \"<br>If you select this option, you'll be taken to the next screen to \"\n            \"select organisations engaged in consortium. You must create the \"\n            \"organisation (<a href='{}'>here</a>) before applying it to this \"\n            \"membership.\"\n        ).format(reverse(\"organization_add\"))\n\n    def save(self, *args, **kwargs):\n        res = super().save(*args, **kwargs)\n\n        create_comment_signal.send(\n            sender=self.__class__,\n            content_object=res,\n            comment=self.cleaned_data[\"comment\"],\n            timestamp=None,\n        )\n\n        return res\n\n\nclass MembershipRollOverForm(MembershipCreateForm):\n    main_organization = None  # remove the additional field\n\n    class Meta(MembershipCreateForm.Meta):\n        fields = [\n            \"name\",\n            \"consortium\",\n            \"public_status\",\n            \"variant\",\n            \"agreement_start\",\n            \"agreement_end\",\n            \"contribution_type\",\n            \"registration_code\",\n            \"agreement_link\",\n            \"workshops_without_admin_fee_per_agreement\",\n            \"workshops_without_admin_fee_rolled_from_previous\",\n            \"public_instructor_training_seats\",\n            \"additional_public_instructor_training_seats\",\n            \"public_instructor_training_seats_rolled_from_previous\",\n            \"inhouse_instructor_training_seats\",\n            \"additional_inhouse_instructor_training_seats\",\n            \"inhouse_instructor_training_seats_rolled_from_previous\",\n            \"emergency_contact\",\n            \"comment\",\n        ]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self[\"workshops_without_admin_fee_rolled_from_previous\"].field.disabled = True\n        self[\n            \"public_instructor_training_seats_rolled_from_previous\"\n        ].field.disabled = True\n        self[\n            \"inhouse_instructor_training_seats_rolled_from_previous\"\n        ].field.disabled = True\n\n\nclass MemberForm(forms.ModelForm):\n    \"\"\"Form intended to use in formset for creating multiple membership members.\"\"\"\n\n    helper = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n    helper_empty_form = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n\n    class Meta:\n        model = Member\n        fields = [\n            \"organization\",\n            \"role\",\n        ]\n        widgets = {\n            \"organization\": ModelSelect2Widget(data_view=\"organization-lookup\"),\n            \"role\": ModelSelect2Widget(data_view=\"memberrole-lookup\"),\n        }\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # set up layout objects for the helpers - they're identical except for\n        # visibility of the delete checkbox\n        self.helper.layout = self.helper.build_default_layout(self)\n        self.helper_empty_form.layout = self.helper.build_default_layout(self)\n        self.helper.layout.append(Field(\"id\"))\n        self.helper.layout.append(Field(\"DELETE\"))  # visible; formset adds it\n        self.helper_empty_form.layout.append(Field(\"id\"))\n        self.helper_empty_form.layout.append(\n            Div(Field(\"DELETE\"), css_class=\"d-none\")  # hidden\n        )\n\n\nclass MembershipTaskForm(forms.ModelForm):\n    \"\"\"Form intended to use in formset for creating multiple membership members.\"\"\"\n\n    helper = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n    helper_empty_form = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n\n    class Meta:\n        model = MembershipTask\n        fields = [\n            \"person\",\n            \"role\",\n        ]\n        widgets = {\n            \"person\": ModelSelect2Widget(data_view=\"person-lookup\"),\n            \"role\": ModelSelect2Widget(data_view=\"membershippersonrole-lookup\"),\n        }\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # set up layout objects for the helpers - they're identical except for\n        # visibility of the delete checkbox\n        self.helper.layout = self.helper.build_default_layout(self)\n        self.helper_empty_form.layout = self.helper.build_default_layout(self)\n        self.helper.layout.append(Field(\"id\"))\n        self.helper.layout.append(Field(\"DELETE\"))  # visible; formset adds it\n        self.helper_empty_form.layout.append(Field(\"id\"))\n        self.helper_empty_form.layout.append(\n            Div(Field(\"DELETE\"), css_class=\"d-none\")  # hidden\n        )\n\n\nclass MembershipExtensionForm(forms.Form):\n    agreement_start = forms.DateField(disabled=True, required=False)\n    agreement_end = forms.DateField(disabled=True, required=False)\n    extension = forms.IntegerField(\n        min_value=1,\n        required=True,\n        help_text=\"Number of days the agreement should be extended.\",\n    )\n    new_agreement_end = forms.DateField(disabled=True, required=False)\n\n    helper = BootstrapHelper()\n\n    class Media:\n        js = (\"membership_extend.js\", \"date_yyyymmdd.js\")\n\n\n# ----------------------------------------------------------\n# Signals\n\n# adding @receiver decorator to the function defined in `workshops.forms`\nform_saved_add_comment = receiver(\n    create_comment_signal,\n    sender=OrganizationCreateForm,\n)(form_saved_add_comment)\n\n# adding @receiver decorator to the function defined in `workshops.forms`\nform_saved_add_comment = receiver(\n    create_comment_signal,\n    sender=MembershipCreateForm,\n)(form_saved_add_comment)\n", "path": "amy/fiscal/forms.py"}], "after_files": [{"content": "from crispy_forms.layout import Div, HTML\nfrom django import forms\nfrom django.core.exceptions import ValidationError\nfrom django.core.validators import RegexValidator\nfrom django.dispatch import receiver\nfrom django.urls import reverse\nfrom markdownx.fields import MarkdownxFormField\n\nfrom fiscal.models import MembershipTask\nfrom workshops.forms import (\n    BootstrapHelper,\n    form_saved_add_comment,\n)\nfrom workshops.models import (\n    Organization,\n    Member,\n    Membership,\n)\n\n# this is used instead of Django Autocomplete Light widgets\n# see issue #1330: https://github.com/swcarpentry/amy/issues/1330\nfrom workshops.fields import (\n    ModelSelect2Widget,\n)\nfrom workshops.signals import create_comment_signal\n\n\n# settings for Select2\n# this makes it possible for autocomplete widget to fit in low-width sidebar\nSIDEBAR_DAL_WIDTH = {\n    \"data-width\": \"100%\",\n    \"width\": \"style\",\n}\n\n\nclass OrganizationForm(forms.ModelForm):\n    domain = forms.CharField(\n        max_length=Organization._meta.get_field(\"domain\").max_length,\n        validators=[\n            RegexValidator(\n                r\"[^\\w\\.-]+\",\n                inverse_match=True,\n                message='Please enter only the domain (such as \"math.esu.edu\")'\n                ' without a leading \"http://\" or a trailing \"/\".',\n            )\n        ],\n    )\n\n    helper = BootstrapHelper(add_cancel_button=False, duplicate_buttons_on_top=True)\n\n    class Meta:\n        model = Organization\n        fields = [\"domain\", \"fullname\", \"country\", \"latitude\", \"longitude\"]\n\n\nclass OrganizationCreateForm(OrganizationForm):\n    comment = MarkdownxFormField(\n        label=\"Comment\",\n        help_text=\"This will be added to comments after the organization \"\n        \"is created.\",\n        widget=forms.Textarea,\n        required=False,\n    )\n\n    class Meta(OrganizationForm.Meta):\n        fields = OrganizationForm.Meta.fields.copy()\n        fields.append(\"comment\")\n\n    def save(self, *args, **kwargs):\n        res = super().save(*args, **kwargs)\n\n        create_comment_signal.send(\n            sender=self.__class__,\n            content_object=res,\n            comment=self.cleaned_data[\"comment\"],\n            timestamp=None,\n        )\n\n        return res\n\n\nclass MembershipForm(forms.ModelForm):\n    helper = BootstrapHelper(add_cancel_button=False)\n\n    class Meta:\n        model = Membership\n        fields = [\n            \"name\",\n            \"consortium\",\n            \"public_status\",\n            \"variant\",\n            \"agreement_start\",\n            \"agreement_end\",\n            \"contribution_type\",\n            \"registration_code\",\n            \"agreement_link\",\n            \"workshops_without_admin_fee_per_agreement\",\n            \"public_instructor_training_seats\",\n            \"additional_public_instructor_training_seats\",\n            \"inhouse_instructor_training_seats\",\n            \"additional_inhouse_instructor_training_seats\",\n            \"emergency_contact\",\n        ]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # set up a layout object for the helper\n        self.helper.layout = self.helper.build_default_layout(self)\n\n        # add warning alert for dates falling within next 2-3 months\n        INVALID_AGREEMENT_DURATION_WARNING = (\n            \"The selected agreement dates fall out of the typical 1-year long duration.\"\n        )\n        pos_index = self.helper.layout.fields.index(\"agreement_end\")\n        self.helper.layout.insert(\n            pos_index + 1,\n            Div(\n                Div(\n                    HTML(INVALID_AGREEMENT_DURATION_WARNING),\n                    css_class=\"alert alert-warning offset-lg-2 col-lg-8 col-12\",\n                ),\n                id=\"agreement_duration_warning\",\n                css_class=\"form-group row d-none\",\n            ),\n        )\n\n    def clean(self):\n        super().clean()\n        errors = dict()\n\n        # validate agreement end date is no sooner than start date\n        agreement_start = self.cleaned_data.get(\"agreement_start\")\n        agreement_end = self.cleaned_data.get(\"agreement_end\")\n        try:\n            if agreement_end < agreement_start:\n                errors[\"agreement_end\"] = ValidationError(\n                    \"Agreement end date can't be sooner than the start date.\"\n                )\n        except TypeError:\n            pass\n\n        # check if multiple members are assigned - then disallow changing to\n        # non-consortium\n        new_consortium = self.cleaned_data.get(\"consortium\")\n        members_count = self.instance.member_set.count()\n        if not new_consortium and members_count > 1:\n            errors[\"consortium\"] = ValidationError(\n                \"Cannot change to non-consortium when there are multiple members \"\n                \"assigned. Remove the members so that at most 1 is left.\"\n            )\n\n        if errors:\n            raise ValidationError(errors)\n\n\nclass MembershipCreateForm(MembershipForm):\n    comment = MarkdownxFormField(\n        label=\"Comment\",\n        help_text=\"This will be added to comments after the membership is created.\",\n        widget=forms.Textarea,\n        required=False,\n    )\n\n    helper = BootstrapHelper(add_cancel_button=True)\n\n    main_organization = forms.ModelChoiceField(\n        Organization.objects.all(),\n        label=\"Main organisation\",\n        required=True,\n        widget=ModelSelect2Widget(data_view=\"organization-lookup\"),\n        help_text=\"Select main organisation (e.g. Signatory in case of consortium).\",\n    )\n\n    class Meta(MembershipForm.Meta):\n        fields = MembershipForm.Meta.fields.copy()\n        fields.insert(0, \"main_organization\")\n        fields.append(\"comment\")\n\n    class Media:\n        js = (\"membership_create.js\",)\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        self.fields[\"consortium\"].help_text += (\n            \"<br>If you select this option, you'll be taken to the next screen to \"\n            \"select organisations engaged in consortium. You must create the \"\n            \"organisation (<a href='{}'>here</a>) before applying it to this \"\n            \"membership.\"\n        ).format(reverse(\"organization_add\"))\n\n    def save(self, *args, **kwargs):\n        res = super().save(*args, **kwargs)\n\n        create_comment_signal.send(\n            sender=self.__class__,\n            content_object=res,\n            comment=self.cleaned_data[\"comment\"],\n            timestamp=None,\n        )\n\n        return res\n\n\nclass MembershipRollOverForm(MembershipCreateForm):\n    main_organization = None  # remove the additional field\n\n    class Meta(MembershipCreateForm.Meta):\n        fields = [\n            \"name\",\n            \"consortium\",\n            \"public_status\",\n            \"variant\",\n            \"agreement_start\",\n            \"agreement_end\",\n            \"contribution_type\",\n            \"registration_code\",\n            \"agreement_link\",\n            \"workshops_without_admin_fee_per_agreement\",\n            \"workshops_without_admin_fee_rolled_from_previous\",\n            \"public_instructor_training_seats\",\n            \"additional_public_instructor_training_seats\",\n            \"public_instructor_training_seats_rolled_from_previous\",\n            \"inhouse_instructor_training_seats\",\n            \"additional_inhouse_instructor_training_seats\",\n            \"inhouse_instructor_training_seats_rolled_from_previous\",\n            \"emergency_contact\",\n            \"comment\",\n        ]\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self[\"workshops_without_admin_fee_rolled_from_previous\"].field.disabled = True\n        self[\n            \"public_instructor_training_seats_rolled_from_previous\"\n        ].field.disabled = True\n        self[\n            \"inhouse_instructor_training_seats_rolled_from_previous\"\n        ].field.disabled = True\n\n\nclass EditableFormsetFormMixin(forms.ModelForm):\n    EDITABLE = forms.BooleanField(\n        label=\"Change\",\n        required=False,\n        widget=forms.CheckboxInput(attrs={\"data-form-editable-check\": \"\"}),\n    )\n\n    def clean(self):\n        if self.has_changed() and not self.cleaned_data[\"EDITABLE\"]:\n            raise ValidationError(\"Form values weren't supposed to be changed.\")\n        return super().clean()\n\n\nclass MemberForm(EditableFormsetFormMixin, forms.ModelForm):\n    \"\"\"Form intended to use in formset for creating multiple membership members.\"\"\"\n\n    helper = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n    helper_empty_form = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n\n    class Meta:\n        model = Member\n        fields = [\n            \"organization\",\n            \"role\",\n        ]\n        widgets = {\n            \"organization\": ModelSelect2Widget(data_view=\"organization-lookup\"),\n            \"role\": ModelSelect2Widget(data_view=\"memberrole-lookup\"),\n        }\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # set up layout objects for the helpers - they're identical except for\n        # visibility of the delete checkbox\n        self.helper.layout = self.helper.build_default_layout(self)\n        self.helper.layout.append(\"id\")\n        self.helper.layout.append(\"DELETE\")  # visible; formset adds it\n        self.helper_empty_form.layout = self.helper.build_default_layout(self)\n        self.helper_empty_form.layout.append(\"id\")\n        self.helper_empty_form.layout.append(\n            Div(\"DELETE\", css_class=\"d-none\")  # hidden\n        )\n        # remove EDITABLE checkbox from empty helper form\n        pos_index = self.helper_empty_form.layout.fields.index(\"EDITABLE\")\n        self.helper_empty_form.layout.pop(pos_index)\n\n\nclass MembershipTaskForm(EditableFormsetFormMixin, forms.ModelForm):\n    \"\"\"Form intended to use in formset for creating multiple membership members.\"\"\"\n\n    helper = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n    helper_empty_form = BootstrapHelper(\n        add_cancel_button=False, form_tag=False, add_submit_button=False\n    )\n\n    class Meta:\n        model = MembershipTask\n        fields = [\n            \"person\",\n            \"role\",\n        ]\n        widgets = {\n            \"person\": ModelSelect2Widget(data_view=\"person-lookup\"),\n            \"role\": ModelSelect2Widget(data_view=\"membershippersonrole-lookup\"),\n        }\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n        # set up layout objects for the helpers - they're identical except for\n        # visibility of the delete checkbox\n        self.helper.layout = self.helper.build_default_layout(self)\n        self.helper.layout.append(\"id\")\n        self.helper.layout.append(\"DELETE\")  # visible; formset adds it\n        self.helper_empty_form.layout = self.helper.build_default_layout(self)\n        self.helper_empty_form.layout.append(\"id\")\n        self.helper_empty_form.layout.append(\n            Div(\"DELETE\", css_class=\"d-none\")  # hidden\n        )\n        # remove EDITABLE checkbox from empty helper form\n        pos_index = self.helper_empty_form.layout.fields.index(\"EDITABLE\")\n        self.helper_empty_form.layout.pop(pos_index)\n\n\nclass MembershipExtensionForm(forms.Form):\n    agreement_start = forms.DateField(disabled=True, required=False)\n    agreement_end = forms.DateField(disabled=True, required=False)\n    extension = forms.IntegerField(\n        min_value=1,\n        required=True,\n        help_text=\"Number of days the agreement should be extended.\",\n    )\n    new_agreement_end = forms.DateField(disabled=True, required=False)\n\n    helper = BootstrapHelper()\n\n    class Media:\n        js = (\"membership_extend.js\", \"date_yyyymmdd.js\")\n\n\n# ----------------------------------------------------------\n# Signals\n\n# adding @receiver decorator to the function defined in `workshops.forms`\nform_saved_add_comment = receiver(\n    create_comment_signal,\n    sender=OrganizationCreateForm,\n)(form_saved_add_comment)\n\n# adding @receiver decorator to the function defined in `workshops.forms`\nform_saved_add_comment = receiver(\n    create_comment_signal,\n    sender=MembershipCreateForm,\n)(form_saved_add_comment)\n", "path": "amy/fiscal/forms.py"}]}