We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Remove option for open training application
We are no longer running the open training application so we should remove this option from AMY.

This includes:

- [ ] remove text about open training at top of form (amy/templates/forms/trainingrequest.html)
- [ ] remove open training info from form submission email (amy/templates/mailing/training_request.html and amy/templates/mailing/training_request.txt)
- [ ] fix UI at top of form to only have "Profile Creation" as an option (so no accordion expanding needed - should be expanded by default; "Application type" text is no longer needed; radio button is not needed)  

We could also remove the `review_process` field from the `TrainingRequest` model. However, keeping it in may make it easier if we decide to go back to having different types of training applications.
 


--- 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/extforms/forms.py`
Content:
```
1 from datetime import date
2 from typing import Iterable, cast
3 from urllib.parse import urlparse
4 
5 from captcha.fields import ReCaptchaField
6 from crispy_forms.layout import HTML, Div, Field, Layout
7 from django import forms
8 from django.core.exceptions import ValidationError
9 from django.db.models.fields import BLANK_CHOICE_DASH
10 from django.http import HttpRequest
11 
12 from consents.forms import option_display_value
13 from consents.models import Term, TrainingRequestConsent
14 from extrequests.forms import (
15     SelfOrganisedSubmissionBaseForm,
16     WorkshopInquiryRequestBaseForm,
17     WorkshopRequestBaseForm,
18 )
19 from extrequests.utils import MemberCodeValidationError, member_code_valid_training
20 from workshops.fields import (
21     CheckboxSelectMultipleWithOthers,
22     RadioSelectWithOther,
23     Select2Widget,
24 )
25 from workshops.forms import BootstrapHelper
26 from workshops.models import TrainingRequest
27 from workshops.utils.feature_flags import feature_flag_enabled
28 
29 
30 class TrainingRequestForm(forms.ModelForm):
31     # agreement fields are moved to the model
32 
33     captcha = ReCaptchaField()
34 
35     helper = BootstrapHelper(wider_labels=True, add_cancel_button=False)
36 
37     code_of_conduct_agreement = forms.BooleanField(
38         required=True,
39         label=TrainingRequest._meta.get_field("code_of_conduct_agreement").verbose_name,
40     )
41 
42     class Meta:
43         model = TrainingRequest
44         fields = (
45             "review_process",
46             "member_code",
47             "member_code_override",
48             "eventbrite_url",
49             "personal",
50             "family",
51             "email",
52             "secondary_email",
53             "github",
54             "occupation",
55             "occupation_other",
56             "affiliation",
57             "location",
58             "country",
59             "underresourced",
60             "domains",
61             "domains_other",
62             "underrepresented",
63             "underrepresented_details",
64             "nonprofit_teaching_experience",
65             "previous_involvement",
66             "previous_training",
67             "previous_training_other",
68             "previous_training_explanation",
69             "previous_experience",
70             "previous_experience_other",
71             "previous_experience_explanation",
72             "programming_language_usage_frequency",
73             "checkout_intent",
74             "teaching_intent",
75             "teaching_frequency_expectation",
76             "teaching_frequency_expectation_other",
77             "max_travelling_frequency",
78             "max_travelling_frequency_other",
79             "reason",
80             "user_notes",
81             # "data_privacy_agreement",
82             "code_of_conduct_agreement",
83         )
84         widgets = {
85             "review_process": forms.RadioSelect(),
86             "occupation": RadioSelectWithOther("occupation_other"),
87             "domains": CheckboxSelectMultipleWithOthers("domains_other"),
88             "underrepresented": forms.RadioSelect(),
89             "previous_involvement": forms.CheckboxSelectMultiple(),
90             "previous_training": RadioSelectWithOther("previous_training_other"),
91             "previous_experience": RadioSelectWithOther("previous_experience_other"),
92             "programming_language_usage_frequency": forms.RadioSelect(),
93             "checkout_intent": forms.RadioSelect(),
94             "teaching_intent": forms.RadioSelect(),
95             "teaching_frequency_expectation": RadioSelectWithOther(
96                 "teaching_frequency_expectation_other"
97             ),
98             "max_travelling_frequency": RadioSelectWithOther(
99                 "max_travelling_frequency_other"
100             ),
101             "country": Select2Widget,
102         }
103 
104     def __init__(self, *args, **kwargs):
105         # request is required for ENFORCE_MEMBER_CODES flag
106         self.request_http = kwargs.pop("request", None)
107         super().__init__(*args, **kwargs)
108 
109         # Only active and required terms.
110         self.terms = (
111             Term.objects.prefetch_active_options()
112             .filter(required_type=Term.PROFILE_REQUIRE_TYPE)
113             .order_by("slug")
114         )
115 
116         self.set_consent_fields(self.terms)
117 
118         # set up a layout object for the helper
119         self.helper.layout = self.helper.build_default_layout(self)
120 
121         self.set_other_fields(self.helper.layout)
122         self.set_fake_required_fields()
123         self.set_accordion(self.helper.layout)
124         self.set_display_member_code_override(visible=False)
125         self.set_hr(self.helper.layout)
126 
127     def set_other_field(self, field_name: str, layout: Layout) -> None:
128         """
129         Set up a field so that it can be displayed as a separate widget.
130         """
131         WidgetType = self._meta.widgets[field_name].__class__  # type: ignore
132         cast(WidgetType, self[field_name].field.widget).other_field = self[
133             f"{field_name}_other"
134         ]
135         layout.fields.remove(f"{field_name}_other")
136 
137     def set_other_fields(self, layout: Layout) -> None:
138         """
139         Set fields that have "Other" counterpart as a separate widget.
140         """
141         # Set up "*WithOther" widgets so that they can display additional
142         # inline fields. The original "*other" fields are removed from the layout.
143         self.set_other_field("occupation", layout)
144         self.set_other_field("domains", layout)
145         self.set_other_field("previous_training", layout)
146         self.set_other_field("previous_experience", layout)
147         self.set_other_field("teaching_frequency_expectation", layout)
148         self.set_other_field("max_travelling_frequency", layout)
149 
150     def set_fake_required_fields(self) -> None:
151         # fake requiredness of the registration code / group name
152         self["member_code"].field.widget.fake_required = True  # type: ignore
153 
154     def set_accordion(self, layout: Layout) -> None:
155         # special accordion display for the review process
156         self["review_process"].field.widget.subfields = {  # type: ignore
157             "preapproved": [
158                 self["member_code"],
159                 self["member_code_override"],
160                 self["eventbrite_url"],
161             ],
162             "open": [],  # this option doesn't require any additional fields
163         }
164         self[
165             "review_process"
166         ].field.widget.notes = TrainingRequest.REVIEW_CHOICES_NOTES  # type: ignore
167 
168         # get current position of `review_process` field
169         pos_index = layout.fields.index("review_process")
170 
171         layout.fields.remove("review_process")
172         layout.fields.remove("member_code")
173         layout.fields.remove("member_code_override")
174         layout.fields.remove("eventbrite_url")
175 
176         # insert div+field at previously saved position
177         layout.insert(
178             pos_index,
179             Div(
180                 Field(
181                     "review_process", template="bootstrap4/layout/radio-accordion.html"
182                 ),
183                 css_class="form-group row",
184             ),
185         )
186 
187     def set_display_member_code_override(self, *, visible: bool) -> None:
188         widget = forms.CheckboxInput() if visible else forms.HiddenInput()
189         self.fields["member_code_override"].widget = widget
190 
191     def set_hr(self, layout: Layout) -> None:
192         # add <HR> around "underrepresented*" fields
193         index = layout.fields.index("underrepresented")
194         layout.insert(index, HTML(self.helper.hr()))
195 
196         index = layout.fields.index("underrepresented_details")
197         layout.insert(index + 1, HTML(self.helper.hr()))
198 
199     def set_consent_fields(self, terms: Iterable[Term]) -> None:
200         for term in terms:
201             self.fields[term.slug] = self.create_consent_field(term)
202 
203     def create_consent_field(self, term: Term) -> forms.ChoiceField:
204         options = [(opt.pk, option_display_value(opt)) for opt in term.options]
205         label = term.training_request_content or term.content
206         required = term.required_type == Term.PROFILE_REQUIRE_TYPE
207         initial = None
208         attrs = {"class": "border border-warning"} if initial is None else {}
209 
210         field = forms.ChoiceField(
211             choices=BLANK_CHOICE_DASH + options,
212             label=label,
213             required=required,
214             initial=initial,
215             help_text=term.help_text or "",
216             widget=forms.Select(attrs=attrs),
217         )
218         return field
219 
220     @feature_flag_enabled("ENFORCE_MEMBER_CODES")
221     def validate_member_code(
222         self, request: HttpRequest
223     ) -> None | dict[str, ValidationError]:
224         errors = dict()
225         member_code = self.cleaned_data.get("member_code", "")
226         member_code_override = self.cleaned_data.get("member_code_override", False)
227         error_msg = (
228             "This code is invalid. "
229             "This may be due to a typo, an expired code, "
230             "a code that has not yet been activated, "
231             "or a code with no training seats remaining. "
232             "Please confirm that you have copied the code correctly, "
233             "or confirm the code with the Membership Contact for your group. "
234             "If the code seems to be correct, tick the checkbox below to ignore "
235             "this message."
236         )
237 
238         if not member_code:
239             return None
240 
241         # check code validity
242         # grace period: 90 days before and after
243         try:
244             member_code_is_valid = member_code_valid_training(
245                 member_code, date.today(), grace_before=90, grace_after=90
246             )
247             if member_code_is_valid and member_code_override:
248                 # case where a user corrects their code but ticks the box anyway
249                 # checkbox doesn't need to be ticked, so correct it quietly and continue
250                 self.cleaned_data["member_code_override"] = False
251                 self.set_display_member_code_override(visible=False)
252         except MemberCodeValidationError:
253             self.set_display_member_code_override(visible=True)
254             if not member_code_override:
255                 # user must either correct the code or tick the override
256                 errors["member_code"] = ValidationError(error_msg)
257 
258         return errors
259 
260     def clean_eventbrite_url(self):
261         """Check that entered URL includes 'eventbrite' in the domain."""
262         eventbrite_url = self.cleaned_data.get("eventbrite_url", "")
263         if eventbrite_url and "eventbrite" not in urlparse(eventbrite_url).hostname:
264             raise ValidationError("Must be an Eventbrite URL.")
265         return eventbrite_url
266 
267     def clean(self):
268         super().clean()
269         errors = dict()
270 
271         # 1: validate registration code
272         review_process = self.cleaned_data.get("review_process", "")
273         member_code = self.cleaned_data.get("member_code", "").split()
274 
275         # it's required when review_process is 'preapproved', but not when
276         # 'open'
277         if review_process == "preapproved" and not member_code:
278             errors["review_process"] = ValidationError(
279                 "Registration code is required for pre-approved training "
280                 "review process."
281             )
282 
283         # it's required to be empty when review_process is 'open'
284         if review_process == "open" and member_code:
285             errors["review_process"] = ValidationError(
286                 "Registration code must be empty for open training review process."
287             )
288 
289         # confirm that code is valid
290         membership_errors = self.validate_member_code(request=self.request_http)
291         if membership_errors:
292             errors.update(membership_errors)
293 
294         if errors:
295             raise ValidationError(errors)
296 
297     def save(self, *args, **kwargs) -> None:
298         training_request = super().save(*args, **kwargs)
299         new_consents: list[TrainingRequestConsent] = []
300         for term in self.terms:
301             option_id = self.cleaned_data.get(term.slug)
302             if not option_id:
303                 continue
304             new_consents.append(
305                 TrainingRequestConsent(
306                     training_request=training_request,
307                     term_option_id=option_id,
308                     term_id=term.pk,
309                 )
310             )
311         TrainingRequestConsent.objects.bulk_create(new_consents)
312         return training_request
313 
314 
315 class WorkshopRequestExternalForm(WorkshopRequestBaseForm):
316     captcha = ReCaptchaField()
317 
318     class Meta(WorkshopRequestBaseForm.Meta):
319         fields = WorkshopRequestBaseForm.Meta.fields + ("captcha",)
320 
321 
322 class WorkshopInquiryRequestExternalForm(WorkshopInquiryRequestBaseForm):
323     captcha = ReCaptchaField()
324 
325     class Meta(WorkshopInquiryRequestBaseForm.Meta):
326         fields = WorkshopInquiryRequestBaseForm.Meta.fields + ("captcha",)
327 
328 
329 class SelfOrganisedSubmissionExternalForm(SelfOrganisedSubmissionBaseForm):
330     captcha = ReCaptchaField()
331 
332     class Meta(SelfOrganisedSubmissionBaseForm.Meta):
333         fields = SelfOrganisedSubmissionBaseForm.Meta.fields + ("captcha",)
334
```
--- 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/extforms/forms.py b/amy/extforms/forms.py
--- a/amy/extforms/forms.py
+++ b/amy/extforms/forms.py
@@ -42,12 +42,11 @@
     class Meta:
         model = TrainingRequest
         fields = (
-            "review_process",
+            "personal",
+            "family",
             "member_code",
             "member_code_override",
             "eventbrite_url",
-            "personal",
-            "family",
             "email",
             "secondary_email",
             "github",
@@ -82,7 +81,6 @@
             "code_of_conduct_agreement",
         )
         widgets = {
-            "review_process": forms.RadioSelect(),
             "occupation": RadioSelectWithOther("occupation_other"),
             "domains": CheckboxSelectMultipleWithOthers("domains_other"),
             "underrepresented": forms.RadioSelect(),
@@ -120,7 +118,6 @@
 
         self.set_other_fields(self.helper.layout)
         self.set_fake_required_fields()
-        self.set_accordion(self.helper.layout)
         self.set_display_member_code_override(visible=False)
         self.set_hr(self.helper.layout)
 
@@ -152,6 +149,8 @@
         self["member_code"].field.widget.fake_required = True  # type: ignore
 
     def set_accordion(self, layout: Layout) -> None:
+        # Note: not used since 2024-03-19 (#2617).
+
         # special accordion display for the review process
         self["review_process"].field.widget.subfields = {  # type: ignore
             "preapproved": [
@@ -268,21 +267,25 @@
         super().clean()
         errors = dict()
 
+        # Since 2024-03-19 (#2617) we don't allow open training applications. All
+        # applications are by default pre-approved.
+        review_process = self.cleaned_data.get("review_process", "preapproved")
+        self.instance.review_process = review_process
+
         # 1: validate registration code
-        review_process = self.cleaned_data.get("review_process", "")
         member_code = self.cleaned_data.get("member_code", "").split()
 
         # it's required when review_process is 'preapproved', but not when
         # 'open'
         if review_process == "preapproved" and not member_code:
-            errors["review_process"] = ValidationError(
+            errors["member_code"] = ValidationError(
                 "Registration code is required for pre-approved training "
                 "review process."
             )
 
         # it's required to be empty when review_process is 'open'
         if review_process == "open" and member_code:
-            errors["review_process"] = ValidationError(
+            errors["member_code"] = ValidationError(
                 "Registration code must be empty for open training review process."
             )

{"golden_diff": "diff --git a/amy/extforms/forms.py b/amy/extforms/forms.py\n--- a/amy/extforms/forms.py\n+++ b/amy/extforms/forms.py\n@@ -42,12 +42,11 @@\n     class Meta:\n         model = TrainingRequest\n         fields = (\n-            \"review_process\",\n+            \"personal\",\n+            \"family\",\n             \"member_code\",\n             \"member_code_override\",\n             \"eventbrite_url\",\n-            \"personal\",\n-            \"family\",\n             \"email\",\n             \"secondary_email\",\n             \"github\",\n@@ -82,7 +81,6 @@\n             \"code_of_conduct_agreement\",\n         )\n         widgets = {\n-            \"review_process\": forms.RadioSelect(),\n             \"occupation\": RadioSelectWithOther(\"occupation_other\"),\n             \"domains\": CheckboxSelectMultipleWithOthers(\"domains_other\"),\n             \"underrepresented\": forms.RadioSelect(),\n@@ -120,7 +118,6 @@\n \n         self.set_other_fields(self.helper.layout)\n         self.set_fake_required_fields()\n-        self.set_accordion(self.helper.layout)\n         self.set_display_member_code_override(visible=False)\n         self.set_hr(self.helper.layout)\n \n@@ -152,6 +149,8 @@\n         self[\"member_code\"].field.widget.fake_required = True  # type: ignore\n \n     def set_accordion(self, layout: Layout) -> None:\n+        # Note: not used since 2024-03-19 (#2617).\n+\n         # special accordion display for the review process\n         self[\"review_process\"].field.widget.subfields = {  # type: ignore\n             \"preapproved\": [\n@@ -268,21 +267,25 @@\n         super().clean()\n         errors = dict()\n \n+        # Since 2024-03-19 (#2617) we don't allow open training applications. All\n+        # applications are by default pre-approved.\n+        review_process = self.cleaned_data.get(\"review_process\", \"preapproved\")\n+        self.instance.review_process = review_process\n+\n         # 1: validate registration code\n-        review_process = self.cleaned_data.get(\"review_process\", \"\")\n         member_code = self.cleaned_data.get(\"member_code\", \"\").split()\n \n         # it's required when review_process is 'preapproved', but not when\n         # 'open'\n         if review_process == \"preapproved\" and not member_code:\n-            errors[\"review_process\"] = ValidationError(\n+            errors[\"member_code\"] = ValidationError(\n                 \"Registration code is required for pre-approved training \"\n                 \"review process.\"\n             )\n \n         # it's required to be empty when review_process is 'open'\n         if review_process == \"open\" and member_code:\n-            errors[\"review_process\"] = ValidationError(\n+            errors[\"member_code\"] = ValidationError(\n                 \"Registration code must be empty for open training review process.\"\n             )\n", "issue": "Remove option for open training application\nWe are no longer running the open training application so we should remove this option from AMY.\r\n\r\nThis includes:\r\n\r\n- [ ] remove text about open training at top of form (amy/templates/forms/trainingrequest.html)\r\n- [ ] remove open training info from form submission email (amy/templates/mailing/training_request.html and amy/templates/mailing/training_request.txt)\r\n- [ ] fix UI at top of form to only have \"Profile Creation\" as an option (so no accordion expanding needed - should be expanded by default; \"Application type\" text is no longer needed; radio button is not needed)  \r\n\r\nWe could also remove the `review_process` field from the `TrainingRequest` model. However, keeping it in may make it easier if we decide to go back to having different types of training applications.\r\n \r\n\n", "before_files": [{"content": "from datetime import date\nfrom typing import Iterable, cast\nfrom urllib.parse import urlparse\n\nfrom captcha.fields import ReCaptchaField\nfrom crispy_forms.layout import HTML, Div, Field, Layout\nfrom django import forms\nfrom django.core.exceptions import ValidationError\nfrom django.db.models.fields import BLANK_CHOICE_DASH\nfrom django.http import HttpRequest\n\nfrom consents.forms import option_display_value\nfrom consents.models import Term, TrainingRequestConsent\nfrom extrequests.forms import (\n    SelfOrganisedSubmissionBaseForm,\n    WorkshopInquiryRequestBaseForm,\n    WorkshopRequestBaseForm,\n)\nfrom extrequests.utils import MemberCodeValidationError, member_code_valid_training\nfrom workshops.fields import (\n    CheckboxSelectMultipleWithOthers,\n    RadioSelectWithOther,\n    Select2Widget,\n)\nfrom workshops.forms import BootstrapHelper\nfrom workshops.models import TrainingRequest\nfrom workshops.utils.feature_flags import feature_flag_enabled\n\n\nclass TrainingRequestForm(forms.ModelForm):\n    # agreement fields are moved to the model\n\n    captcha = ReCaptchaField()\n\n    helper = BootstrapHelper(wider_labels=True, add_cancel_button=False)\n\n    code_of_conduct_agreement = forms.BooleanField(\n        required=True,\n        label=TrainingRequest._meta.get_field(\"code_of_conduct_agreement\").verbose_name,\n    )\n\n    class Meta:\n        model = TrainingRequest\n        fields = (\n            \"review_process\",\n            \"member_code\",\n            \"member_code_override\",\n            \"eventbrite_url\",\n            \"personal\",\n            \"family\",\n            \"email\",\n            \"secondary_email\",\n            \"github\",\n            \"occupation\",\n            \"occupation_other\",\n            \"affiliation\",\n            \"location\",\n            \"country\",\n            \"underresourced\",\n            \"domains\",\n            \"domains_other\",\n            \"underrepresented\",\n            \"underrepresented_details\",\n            \"nonprofit_teaching_experience\",\n            \"previous_involvement\",\n            \"previous_training\",\n            \"previous_training_other\",\n            \"previous_training_explanation\",\n            \"previous_experience\",\n            \"previous_experience_other\",\n            \"previous_experience_explanation\",\n            \"programming_language_usage_frequency\",\n            \"checkout_intent\",\n            \"teaching_intent\",\n            \"teaching_frequency_expectation\",\n            \"teaching_frequency_expectation_other\",\n            \"max_travelling_frequency\",\n            \"max_travelling_frequency_other\",\n            \"reason\",\n            \"user_notes\",\n            # \"data_privacy_agreement\",\n            \"code_of_conduct_agreement\",\n        )\n        widgets = {\n            \"review_process\": forms.RadioSelect(),\n            \"occupation\": RadioSelectWithOther(\"occupation_other\"),\n            \"domains\": CheckboxSelectMultipleWithOthers(\"domains_other\"),\n            \"underrepresented\": forms.RadioSelect(),\n            \"previous_involvement\": forms.CheckboxSelectMultiple(),\n            \"previous_training\": RadioSelectWithOther(\"previous_training_other\"),\n            \"previous_experience\": RadioSelectWithOther(\"previous_experience_other\"),\n            \"programming_language_usage_frequency\": forms.RadioSelect(),\n            \"checkout_intent\": forms.RadioSelect(),\n            \"teaching_intent\": forms.RadioSelect(),\n            \"teaching_frequency_expectation\": RadioSelectWithOther(\n                \"teaching_frequency_expectation_other\"\n            ),\n            \"max_travelling_frequency\": RadioSelectWithOther(\n                \"max_travelling_frequency_other\"\n            ),\n            \"country\": Select2Widget,\n        }\n\n    def __init__(self, *args, **kwargs):\n        # request is required for ENFORCE_MEMBER_CODES flag\n        self.request_http = kwargs.pop(\"request\", None)\n        super().__init__(*args, **kwargs)\n\n        # Only active and required terms.\n        self.terms = (\n            Term.objects.prefetch_active_options()\n            .filter(required_type=Term.PROFILE_REQUIRE_TYPE)\n            .order_by(\"slug\")\n        )\n\n        self.set_consent_fields(self.terms)\n\n        # set up a layout object for the helper\n        self.helper.layout = self.helper.build_default_layout(self)\n\n        self.set_other_fields(self.helper.layout)\n        self.set_fake_required_fields()\n        self.set_accordion(self.helper.layout)\n        self.set_display_member_code_override(visible=False)\n        self.set_hr(self.helper.layout)\n\n    def set_other_field(self, field_name: str, layout: Layout) -> None:\n        \"\"\"\n        Set up a field so that it can be displayed as a separate widget.\n        \"\"\"\n        WidgetType = self._meta.widgets[field_name].__class__  # type: ignore\n        cast(WidgetType, self[field_name].field.widget).other_field = self[\n            f\"{field_name}_other\"\n        ]\n        layout.fields.remove(f\"{field_name}_other\")\n\n    def set_other_fields(self, layout: Layout) -> None:\n        \"\"\"\n        Set fields that have \"Other\" counterpart as a separate widget.\n        \"\"\"\n        # Set up \"*WithOther\" widgets so that they can display additional\n        # inline fields. The original \"*other\" fields are removed from the layout.\n        self.set_other_field(\"occupation\", layout)\n        self.set_other_field(\"domains\", layout)\n        self.set_other_field(\"previous_training\", layout)\n        self.set_other_field(\"previous_experience\", layout)\n        self.set_other_field(\"teaching_frequency_expectation\", layout)\n        self.set_other_field(\"max_travelling_frequency\", layout)\n\n    def set_fake_required_fields(self) -> None:\n        # fake requiredness of the registration code / group name\n        self[\"member_code\"].field.widget.fake_required = True  # type: ignore\n\n    def set_accordion(self, layout: Layout) -> None:\n        # special accordion display for the review process\n        self[\"review_process\"].field.widget.subfields = {  # type: ignore\n            \"preapproved\": [\n                self[\"member_code\"],\n                self[\"member_code_override\"],\n                self[\"eventbrite_url\"],\n            ],\n            \"open\": [],  # this option doesn't require any additional fields\n        }\n        self[\n            \"review_process\"\n        ].field.widget.notes = TrainingRequest.REVIEW_CHOICES_NOTES  # type: ignore\n\n        # get current position of `review_process` field\n        pos_index = layout.fields.index(\"review_process\")\n\n        layout.fields.remove(\"review_process\")\n        layout.fields.remove(\"member_code\")\n        layout.fields.remove(\"member_code_override\")\n        layout.fields.remove(\"eventbrite_url\")\n\n        # insert div+field at previously saved position\n        layout.insert(\n            pos_index,\n            Div(\n                Field(\n                    \"review_process\", template=\"bootstrap4/layout/radio-accordion.html\"\n                ),\n                css_class=\"form-group row\",\n            ),\n        )\n\n    def set_display_member_code_override(self, *, visible: bool) -> None:\n        widget = forms.CheckboxInput() if visible else forms.HiddenInput()\n        self.fields[\"member_code_override\"].widget = widget\n\n    def set_hr(self, layout: Layout) -> None:\n        # add <HR> around \"underrepresented*\" fields\n        index = layout.fields.index(\"underrepresented\")\n        layout.insert(index, HTML(self.helper.hr()))\n\n        index = layout.fields.index(\"underrepresented_details\")\n        layout.insert(index + 1, HTML(self.helper.hr()))\n\n    def set_consent_fields(self, terms: Iterable[Term]) -> None:\n        for term in terms:\n            self.fields[term.slug] = self.create_consent_field(term)\n\n    def create_consent_field(self, term: Term) -> forms.ChoiceField:\n        options = [(opt.pk, option_display_value(opt)) for opt in term.options]\n        label = term.training_request_content or term.content\n        required = term.required_type == Term.PROFILE_REQUIRE_TYPE\n        initial = None\n        attrs = {\"class\": \"border border-warning\"} if initial is None else {}\n\n        field = forms.ChoiceField(\n            choices=BLANK_CHOICE_DASH + options,\n            label=label,\n            required=required,\n            initial=initial,\n            help_text=term.help_text or \"\",\n            widget=forms.Select(attrs=attrs),\n        )\n        return field\n\n    @feature_flag_enabled(\"ENFORCE_MEMBER_CODES\")\n    def validate_member_code(\n        self, request: HttpRequest\n    ) -> None | dict[str, ValidationError]:\n        errors = dict()\n        member_code = self.cleaned_data.get(\"member_code\", \"\")\n        member_code_override = self.cleaned_data.get(\"member_code_override\", False)\n        error_msg = (\n            \"This code is invalid. \"\n            \"This may be due to a typo, an expired code, \"\n            \"a code that has not yet been activated, \"\n            \"or a code with no training seats remaining. \"\n            \"Please confirm that you have copied the code correctly, \"\n            \"or confirm the code with the Membership Contact for your group. \"\n            \"If the code seems to be correct, tick the checkbox below to ignore \"\n            \"this message.\"\n        )\n\n        if not member_code:\n            return None\n\n        # check code validity\n        # grace period: 90 days before and after\n        try:\n            member_code_is_valid = member_code_valid_training(\n                member_code, date.today(), grace_before=90, grace_after=90\n            )\n            if member_code_is_valid and member_code_override:\n                # case where a user corrects their code but ticks the box anyway\n                # checkbox doesn't need to be ticked, so correct it quietly and continue\n                self.cleaned_data[\"member_code_override\"] = False\n                self.set_display_member_code_override(visible=False)\n        except MemberCodeValidationError:\n            self.set_display_member_code_override(visible=True)\n            if not member_code_override:\n                # user must either correct the code or tick the override\n                errors[\"member_code\"] = ValidationError(error_msg)\n\n        return errors\n\n    def clean_eventbrite_url(self):\n        \"\"\"Check that entered URL includes 'eventbrite' in the domain.\"\"\"\n        eventbrite_url = self.cleaned_data.get(\"eventbrite_url\", \"\")\n        if eventbrite_url and \"eventbrite\" not in urlparse(eventbrite_url).hostname:\n            raise ValidationError(\"Must be an Eventbrite URL.\")\n        return eventbrite_url\n\n    def clean(self):\n        super().clean()\n        errors = dict()\n\n        # 1: validate registration code\n        review_process = self.cleaned_data.get(\"review_process\", \"\")\n        member_code = self.cleaned_data.get(\"member_code\", \"\").split()\n\n        # it's required when review_process is 'preapproved', but not when\n        # 'open'\n        if review_process == \"preapproved\" and not member_code:\n            errors[\"review_process\"] = ValidationError(\n                \"Registration code is required for pre-approved training \"\n                \"review process.\"\n            )\n\n        # it's required to be empty when review_process is 'open'\n        if review_process == \"open\" and member_code:\n            errors[\"review_process\"] = ValidationError(\n                \"Registration code must be empty for open training review process.\"\n            )\n\n        # confirm that code is valid\n        membership_errors = self.validate_member_code(request=self.request_http)\n        if membership_errors:\n            errors.update(membership_errors)\n\n        if errors:\n            raise ValidationError(errors)\n\n    def save(self, *args, **kwargs) -> None:\n        training_request = super().save(*args, **kwargs)\n        new_consents: list[TrainingRequestConsent] = []\n        for term in self.terms:\n            option_id = self.cleaned_data.get(term.slug)\n            if not option_id:\n                continue\n            new_consents.append(\n                TrainingRequestConsent(\n                    training_request=training_request,\n                    term_option_id=option_id,\n                    term_id=term.pk,\n                )\n            )\n        TrainingRequestConsent.objects.bulk_create(new_consents)\n        return training_request\n\n\nclass WorkshopRequestExternalForm(WorkshopRequestBaseForm):\n    captcha = ReCaptchaField()\n\n    class Meta(WorkshopRequestBaseForm.Meta):\n        fields = WorkshopRequestBaseForm.Meta.fields + (\"captcha\",)\n\n\nclass WorkshopInquiryRequestExternalForm(WorkshopInquiryRequestBaseForm):\n    captcha = ReCaptchaField()\n\n    class Meta(WorkshopInquiryRequestBaseForm.Meta):\n        fields = WorkshopInquiryRequestBaseForm.Meta.fields + (\"captcha\",)\n\n\nclass SelfOrganisedSubmissionExternalForm(SelfOrganisedSubmissionBaseForm):\n    captcha = ReCaptchaField()\n\n    class Meta(SelfOrganisedSubmissionBaseForm.Meta):\n        fields = SelfOrganisedSubmissionBaseForm.Meta.fields + (\"captcha\",)\n", "path": "amy/extforms/forms.py"}], "after_files": [{"content": "from datetime import date\nfrom typing import Iterable, cast\nfrom urllib.parse import urlparse\n\nfrom captcha.fields import ReCaptchaField\nfrom crispy_forms.layout import HTML, Div, Field, Layout\nfrom django import forms\nfrom django.core.exceptions import ValidationError\nfrom django.db.models.fields import BLANK_CHOICE_DASH\nfrom django.http import HttpRequest\n\nfrom consents.forms import option_display_value\nfrom consents.models import Term, TrainingRequestConsent\nfrom extrequests.forms import (\n    SelfOrganisedSubmissionBaseForm,\n    WorkshopInquiryRequestBaseForm,\n    WorkshopRequestBaseForm,\n)\nfrom extrequests.utils import MemberCodeValidationError, member_code_valid_training\nfrom workshops.fields import (\n    CheckboxSelectMultipleWithOthers,\n    RadioSelectWithOther,\n    Select2Widget,\n)\nfrom workshops.forms import BootstrapHelper\nfrom workshops.models import TrainingRequest\nfrom workshops.utils.feature_flags import feature_flag_enabled\n\n\nclass TrainingRequestForm(forms.ModelForm):\n    # agreement fields are moved to the model\n\n    captcha = ReCaptchaField()\n\n    helper = BootstrapHelper(wider_labels=True, add_cancel_button=False)\n\n    code_of_conduct_agreement = forms.BooleanField(\n        required=True,\n        label=TrainingRequest._meta.get_field(\"code_of_conduct_agreement\").verbose_name,\n    )\n\n    class Meta:\n        model = TrainingRequest\n        fields = (\n            \"personal\",\n            \"family\",\n            \"member_code\",\n            \"member_code_override\",\n            \"eventbrite_url\",\n            \"email\",\n            \"secondary_email\",\n            \"github\",\n            \"occupation\",\n            \"occupation_other\",\n            \"affiliation\",\n            \"location\",\n            \"country\",\n            \"underresourced\",\n            \"domains\",\n            \"domains_other\",\n            \"underrepresented\",\n            \"underrepresented_details\",\n            \"nonprofit_teaching_experience\",\n            \"previous_involvement\",\n            \"previous_training\",\n            \"previous_training_other\",\n            \"previous_training_explanation\",\n            \"previous_experience\",\n            \"previous_experience_other\",\n            \"previous_experience_explanation\",\n            \"programming_language_usage_frequency\",\n            \"checkout_intent\",\n            \"teaching_intent\",\n            \"teaching_frequency_expectation\",\n            \"teaching_frequency_expectation_other\",\n            \"max_travelling_frequency\",\n            \"max_travelling_frequency_other\",\n            \"reason\",\n            \"user_notes\",\n            # \"data_privacy_agreement\",\n            \"code_of_conduct_agreement\",\n        )\n        widgets = {\n            \"occupation\": RadioSelectWithOther(\"occupation_other\"),\n            \"domains\": CheckboxSelectMultipleWithOthers(\"domains_other\"),\n            \"underrepresented\": forms.RadioSelect(),\n            \"previous_involvement\": forms.CheckboxSelectMultiple(),\n            \"previous_training\": RadioSelectWithOther(\"previous_training_other\"),\n            \"previous_experience\": RadioSelectWithOther(\"previous_experience_other\"),\n            \"programming_language_usage_frequency\": forms.RadioSelect(),\n            \"checkout_intent\": forms.RadioSelect(),\n            \"teaching_intent\": forms.RadioSelect(),\n            \"teaching_frequency_expectation\": RadioSelectWithOther(\n                \"teaching_frequency_expectation_other\"\n            ),\n            \"max_travelling_frequency\": RadioSelectWithOther(\n                \"max_travelling_frequency_other\"\n            ),\n            \"country\": Select2Widget,\n        }\n\n    def __init__(self, *args, **kwargs):\n        # request is required for ENFORCE_MEMBER_CODES flag\n        self.request_http = kwargs.pop(\"request\", None)\n        super().__init__(*args, **kwargs)\n\n        # Only active and required terms.\n        self.terms = (\n            Term.objects.prefetch_active_options()\n            .filter(required_type=Term.PROFILE_REQUIRE_TYPE)\n            .order_by(\"slug\")\n        )\n\n        self.set_consent_fields(self.terms)\n\n        # set up a layout object for the helper\n        self.helper.layout = self.helper.build_default_layout(self)\n\n        self.set_other_fields(self.helper.layout)\n        self.set_fake_required_fields()\n        self.set_display_member_code_override(visible=False)\n        self.set_hr(self.helper.layout)\n\n    def set_other_field(self, field_name: str, layout: Layout) -> None:\n        \"\"\"\n        Set up a field so that it can be displayed as a separate widget.\n        \"\"\"\n        WidgetType = self._meta.widgets[field_name].__class__  # type: ignore\n        cast(WidgetType, self[field_name].field.widget).other_field = self[\n            f\"{field_name}_other\"\n        ]\n        layout.fields.remove(f\"{field_name}_other\")\n\n    def set_other_fields(self, layout: Layout) -> None:\n        \"\"\"\n        Set fields that have \"Other\" counterpart as a separate widget.\n        \"\"\"\n        # Set up \"*WithOther\" widgets so that they can display additional\n        # inline fields. The original \"*other\" fields are removed from the layout.\n        self.set_other_field(\"occupation\", layout)\n        self.set_other_field(\"domains\", layout)\n        self.set_other_field(\"previous_training\", layout)\n        self.set_other_field(\"previous_experience\", layout)\n        self.set_other_field(\"teaching_frequency_expectation\", layout)\n        self.set_other_field(\"max_travelling_frequency\", layout)\n\n    def set_fake_required_fields(self) -> None:\n        # fake requiredness of the registration code / group name\n        self[\"member_code\"].field.widget.fake_required = True  # type: ignore\n\n    def set_accordion(self, layout: Layout) -> None:\n        # Note: not used since 2024-03-19 (#2617).\n\n        # special accordion display for the review process\n        self[\"review_process\"].field.widget.subfields = {  # type: ignore\n            \"preapproved\": [\n                self[\"member_code\"],\n                self[\"member_code_override\"],\n                self[\"eventbrite_url\"],\n            ],\n            \"open\": [],  # this option doesn't require any additional fields\n        }\n        self[\n            \"review_process\"\n        ].field.widget.notes = TrainingRequest.REVIEW_CHOICES_NOTES  # type: ignore\n\n        # get current position of `review_process` field\n        pos_index = layout.fields.index(\"review_process\")\n\n        layout.fields.remove(\"review_process\")\n        layout.fields.remove(\"member_code\")\n        layout.fields.remove(\"member_code_override\")\n        layout.fields.remove(\"eventbrite_url\")\n\n        # insert div+field at previously saved position\n        layout.insert(\n            pos_index,\n            Div(\n                Field(\n                    \"review_process\", template=\"bootstrap4/layout/radio-accordion.html\"\n                ),\n                css_class=\"form-group row\",\n            ),\n        )\n\n    def set_display_member_code_override(self, *, visible: bool) -> None:\n        widget = forms.CheckboxInput() if visible else forms.HiddenInput()\n        self.fields[\"member_code_override\"].widget = widget\n\n    def set_hr(self, layout: Layout) -> None:\n        # add <HR> around \"underrepresented*\" fields\n        index = layout.fields.index(\"underrepresented\")\n        layout.insert(index, HTML(self.helper.hr()))\n\n        index = layout.fields.index(\"underrepresented_details\")\n        layout.insert(index + 1, HTML(self.helper.hr()))\n\n    def set_consent_fields(self, terms: Iterable[Term]) -> None:\n        for term in terms:\n            self.fields[term.slug] = self.create_consent_field(term)\n\n    def create_consent_field(self, term: Term) -> forms.ChoiceField:\n        options = [(opt.pk, option_display_value(opt)) for opt in term.options]\n        label = term.training_request_content or term.content\n        required = term.required_type == Term.PROFILE_REQUIRE_TYPE\n        initial = None\n        attrs = {\"class\": \"border border-warning\"} if initial is None else {}\n\n        field = forms.ChoiceField(\n            choices=BLANK_CHOICE_DASH + options,\n            label=label,\n            required=required,\n            initial=initial,\n            help_text=term.help_text or \"\",\n            widget=forms.Select(attrs=attrs),\n        )\n        return field\n\n    @feature_flag_enabled(\"ENFORCE_MEMBER_CODES\")\n    def validate_member_code(\n        self, request: HttpRequest\n    ) -> None | dict[str, ValidationError]:\n        errors = dict()\n        member_code = self.cleaned_data.get(\"member_code\", \"\")\n        member_code_override = self.cleaned_data.get(\"member_code_override\", False)\n        error_msg = (\n            \"This code is invalid. \"\n            \"This may be due to a typo, an expired code, \"\n            \"a code that has not yet been activated, \"\n            \"or a code with no training seats remaining. \"\n            \"Please confirm that you have copied the code correctly, \"\n            \"or confirm the code with the Membership Contact for your group. \"\n            \"If the code seems to be correct, tick the checkbox below to ignore \"\n            \"this message.\"\n        )\n\n        if not member_code:\n            return None\n\n        # check code validity\n        # grace period: 90 days before and after\n        try:\n            member_code_is_valid = member_code_valid_training(\n                member_code, date.today(), grace_before=90, grace_after=90\n            )\n            if member_code_is_valid and member_code_override:\n                # case where a user corrects their code but ticks the box anyway\n                # checkbox doesn't need to be ticked, so correct it quietly and continue\n                self.cleaned_data[\"member_code_override\"] = False\n                self.set_display_member_code_override(visible=False)\n        except MemberCodeValidationError:\n            self.set_display_member_code_override(visible=True)\n            if not member_code_override:\n                # user must either correct the code or tick the override\n                errors[\"member_code\"] = ValidationError(error_msg)\n\n        return errors\n\n    def clean_eventbrite_url(self):\n        \"\"\"Check that entered URL includes 'eventbrite' in the domain.\"\"\"\n        eventbrite_url = self.cleaned_data.get(\"eventbrite_url\", \"\")\n        if eventbrite_url and \"eventbrite\" not in urlparse(eventbrite_url).hostname:\n            raise ValidationError(\"Must be an Eventbrite URL.\")\n        return eventbrite_url\n\n    def clean(self):\n        super().clean()\n        errors = dict()\n\n        # Since 2024-03-19 (#2617) we don't allow open training applications. All\n        # applications are by default pre-approved.\n        review_process = self.cleaned_data.get(\"review_process\", \"preapproved\")\n        self.instance.review_process = review_process\n\n        # 1: validate registration code\n        member_code = self.cleaned_data.get(\"member_code\", \"\").split()\n\n        # it's required when review_process is 'preapproved', but not when\n        # 'open'\n        if review_process == \"preapproved\" and not member_code:\n            errors[\"member_code\"] = ValidationError(\n                \"Registration code is required for pre-approved training \"\n                \"review process.\"\n            )\n\n        # it's required to be empty when review_process is 'open'\n        if review_process == \"open\" and member_code:\n            errors[\"member_code\"] = ValidationError(\n                \"Registration code must be empty for open training review process.\"\n            )\n\n        # confirm that code is valid\n        membership_errors = self.validate_member_code(request=self.request_http)\n        if membership_errors:\n            errors.update(membership_errors)\n\n        if errors:\n            raise ValidationError(errors)\n\n    def save(self, *args, **kwargs) -> None:\n        training_request = super().save(*args, **kwargs)\n        new_consents: list[TrainingRequestConsent] = []\n        for term in self.terms:\n            option_id = self.cleaned_data.get(term.slug)\n            if not option_id:\n                continue\n            new_consents.append(\n                TrainingRequestConsent(\n                    training_request=training_request,\n                    term_option_id=option_id,\n                    term_id=term.pk,\n                )\n            )\n        TrainingRequestConsent.objects.bulk_create(new_consents)\n        return training_request\n\n\nclass WorkshopRequestExternalForm(WorkshopRequestBaseForm):\n    captcha = ReCaptchaField()\n\n    class Meta(WorkshopRequestBaseForm.Meta):\n        fields = WorkshopRequestBaseForm.Meta.fields + (\"captcha\",)\n\n\nclass WorkshopInquiryRequestExternalForm(WorkshopInquiryRequestBaseForm):\n    captcha = ReCaptchaField()\n\n    class Meta(WorkshopInquiryRequestBaseForm.Meta):\n        fields = WorkshopInquiryRequestBaseForm.Meta.fields + (\"captcha\",)\n\n\nclass SelfOrganisedSubmissionExternalForm(SelfOrganisedSubmissionBaseForm):\n    captcha = ReCaptchaField()\n\n    class Meta(SelfOrganisedSubmissionBaseForm.Meta):\n        fields = SelfOrganisedSubmissionBaseForm.Meta.fields + (\"captcha\",)\n", "path": "amy/extforms/forms.py"}]}