We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Maximum number of participants for SectionBasedSignup


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ephios/plugins/basesignup/signup/section_based.py`
Content:
```
1 import uuid
2 from functools import cached_property
3 from itertools import groupby
4 from operator import itemgetter
5 
6 from django import forms
7 from django.contrib import messages
8 from django.core.exceptions import ValidationError
9 from django.shortcuts import redirect
10 from django.template.loader import get_template
11 from django.urls import reverse
12 from django.utils.translation import gettext_lazy as _
13 from django.views.generic import FormView
14 from django_select2.forms import Select2MultipleWidget
15 from dynamic_preferences.registries import global_preferences_registry
16 
17 from ephios.core.models import AbstractParticipation, Qualification
18 from ephios.core.signup import (
19     AbstractParticipant,
20     BaseDispositionParticipationForm,
21     BaseSignupMethod,
22     BaseSignupView,
23     ParticipationError,
24 )
25 
26 
27 def sections_participant_qualifies_for(sections, participant: AbstractParticipant):
28     available_qualification_ids = set(q.id for q in participant.collect_all_qualifications())
29     return [
30         section
31         for section in sections
32         if set(section["qualifications"]) <= available_qualification_ids
33     ]
34 
35 
36 class SectionBasedDispositionParticipationForm(BaseDispositionParticipationForm):
37     disposition_participation_template = "basesignup/section_based/fragment_participant.html"
38 
39     section = forms.ChoiceField(
40         label=_("Section"),
41         required=False,  # only required if participation is confirmed
42         widget=forms.Select(
43             attrs={"data-show-for-state": str(AbstractParticipation.States.CONFIRMED)}
44         ),
45     )
46 
47     def __init__(self, **kwargs):
48         super().__init__(**kwargs)
49         sections = self.shift.signup_method.configuration.sections
50         qualified_sections = list(
51             sections_participant_qualifies_for(
52                 sections,
53                 self.instance.participant,
54             )
55         )
56         unqualified_sections = [
57             section for section in sections if section not in qualified_sections
58         ]
59         self.fields["section"].choices = [("", "---")]
60         if qualified_sections:
61             self.fields["section"].choices += [
62                 (
63                     _("qualified"),
64                     [(section["uuid"], section["title"]) for section in qualified_sections],
65                 )
66             ]
67         if unqualified_sections:
68             self.fields["section"].choices += [
69                 (
70                     _("unqualified"),
71                     [(section["uuid"], section["title"]) for section in unqualified_sections],
72                 )
73             ]
74         if preferred_section_uuid := self.instance.data.get("preferred_section_uuid"):
75             self.fields["section"].initial = preferred_section_uuid
76             self.preferred_section = next(
77                 filter(lambda section: section["uuid"] == preferred_section_uuid, sections), None
78             )
79         if initial := self.instance.data.get("dispatched_section_uuid"):
80             self.fields["section"].initial = initial
81 
82     def clean(self):
83         super().clean()
84         if (
85             self.cleaned_data["state"] == AbstractParticipation.States.CONFIRMED
86             and not self.cleaned_data["section"]
87         ):
88             self.add_error(
89                 "section",
90                 ValidationError(_("You must select a section when confirming a participation.")),
91             )
92 
93     def save(self, commit=True):
94         self.instance.data["dispatched_section_uuid"] = self.cleaned_data["section"]
95         super().save(commit)
96 
97 
98 class SectionForm(forms.Form):
99     title = forms.CharField(label=_("Title"), required=True)
100     qualifications = forms.ModelMultipleChoiceField(
101         label=_("Required Qualifications"),
102         queryset=Qualification.objects.all(),
103         widget=Select2MultipleWidget,
104         required=False,
105     )
106     min_count = forms.IntegerField(label=_("min amount"), min_value=0, required=True)
107     uuid = forms.CharField(widget=forms.HiddenInput, required=False)
108 
109     def clean_uuid(self):
110         return self.cleaned_data.get("uuid") or uuid.uuid4()
111 
112 
113 SectionsFormset = forms.formset_factory(
114     SectionForm, can_delete=True, min_num=1, validate_min=1, extra=0
115 )
116 
117 
118 class SectionBasedConfigurationForm(forms.Form):
119     def __init__(self, data=None, **kwargs):
120         super().__init__(data, **kwargs)
121         self.sections_formset = SectionsFormset(
122             data=data,
123             initial=self.initial.get("sections", list()),
124             prefix="sections",
125         )
126 
127     def clean_sections(self):
128         if not self.sections_formset.is_valid():
129             raise ValidationError(_("The sections aren't configured correctly."))
130 
131         sections = [
132             {
133                 key: form.cleaned_data[key]
134                 for key in ("title", "qualifications", "min_count", "uuid")
135             }
136             for form in self.sections_formset
137             if not form.cleaned_data.get("DELETE")
138         ]
139         return sections
140 
141 
142 class SectionSignupForm(forms.Form):
143     section = forms.ChoiceField(
144         label=_("Preferred Section"),
145         widget=forms.RadioSelect,
146         required=False,
147         # choices are set as (uuid, title) of section
148     )
149 
150 
151 class SectionBasedSignupView(FormView, BaseSignupView):
152     template_name = "basesignup/section_based/signup.html"
153 
154     @cached_property
155     def sections_participant_qualifies_for(self):
156         return sections_participant_qualifies_for(
157             self.method.configuration.sections, self.participant
158         )
159 
160     def get_form(self, form_class=None):
161         form = SectionSignupForm(self.request.POST)
162         form.fields["section"].choices = [
163             (section["uuid"], section["title"])
164             for section in self.sections_participant_qualifies_for
165         ]
166         return form
167 
168     def get_context_data(self, **kwargs):
169         kwargs.setdefault("shift", self.shift)
170         kwargs.setdefault(
171             "unqualified_sections",
172             [
173                 section["title"]
174                 for section in self.method.configuration.sections
175                 if section not in self.sections_participant_qualifies_for
176             ],
177         )
178         return super().get_context_data(**kwargs)
179 
180     def form_valid(self, form):
181         return super().signup_pressed(preferred_section_uuid=form.cleaned_data.get("section"))
182 
183     def signup_pressed(self, **kwargs):
184         if not self.method.configuration.choose_preferred_section:
185             # do straight signup if choosing is not enabled
186             return super().signup_pressed(**kwargs)
187 
188         if not self.method.can_sign_up(self.participant):
189             # redirect a misled request
190             messages.warning(self.request, _("You can not sign up for this shift."))
191             return redirect(self.participant.reverse_event_detail(self.shift.event))
192 
193         # all good, redirect to the form
194         return redirect(self.participant.reverse_signup_action(self.shift))
195 
196 
197 class SectionBasedSignupMethod(BaseSignupMethod):
198     slug = "section_based"
199     verbose_name = _("Apply for sections")
200     description = _(
201         """This method lets you define sections for which people can choose from.
202         Sections contain qualifications that helpers need to fulfil."""
203     )
204     registration_button_text = _("Request")
205     signup_success_message = _("You have successfully requested a participation for {shift}.")
206     signup_error_message = _("Requesting a participation failed: {error}")
207 
208     configuration_form_class = SectionBasedConfigurationForm
209     signup_view_class = SectionBasedSignupView
210 
211     disposition_participation_form_class = SectionBasedDispositionParticipationForm
212 
213     def get_configuration_fields(self):
214         return {
215             **super().get_configuration_fields(),
216             "choose_preferred_section": {
217                 "formfield": forms.BooleanField(
218                     label=_("Ask participants for a preferred section"),
219                     help_text=_("This only makes sense if you configure multiple sections."),
220                     widget=forms.CheckboxInput,
221                     required=False,
222                 ),
223                 "default": False,
224             },
225             "sections": {
226                 "formfield": forms.Field(
227                     label=_("Structure"),
228                     widget=forms.HiddenInput,
229                     required=False,
230                 ),
231                 "default": [],
232             },
233         }
234 
235     def get_participant_count_bounds(self):
236         return sum(section.get("min_count") or 0 for section in self.configuration.sections), None
237 
238     @staticmethod
239     def check_qualification(method, participant):
240         if not sections_participant_qualifies_for(method.configuration.sections, participant):
241             return ParticipationError(_("You are not qualified."))
242 
243     @property
244     def _signup_checkers(self):
245         return super()._signup_checkers + [self.check_qualification]
246 
247     # pylint: disable=arguments-differ
248     def _configure_participation(
249         self, participation: AbstractParticipation, preferred_section_uuid=None, **kwargs
250     ) -> AbstractParticipation:
251         participation.data["preferred_section_uuid"] = preferred_section_uuid
252         if preferred_section_uuid:
253             # reset dispatch decision, as that would have overwritten the preferred choice
254             participation.data["dispatched_section_uuid"] = None
255         participation.state = AbstractParticipation.States.REQUESTED
256         return participation
257 
258     def render_configuration_form(self, *args, form=None, **kwargs):
259         form = form or self.get_configuration_form(*args, **kwargs)
260         template = get_template("basesignup/section_based/configuration_form.html").render(
261             {"form": form}
262         )
263         return template
264 
265     def _get_sections_with_users(self):
266         relevant_qualification_categories = global_preferences_registry.manager()[
267             "general__relevant_qualification_categories"
268         ]
269         section_by_uuid = {section["uuid"]: section for section in self.configuration.sections}
270         # get name and preferred section uuid for confirmed participants
271         # if they have a section assigned and we have that section on record
272         confirmed_participations = [
273             {
274                 "name": str(participation.participant),
275                 "relevant_qualifications": ", ".join(
276                     participation.participant.qualifications.filter(
277                         category__in=relevant_qualification_categories
278                     )
279                     .order_by("category", "abbreviation")
280                     .values_list("abbreviation", flat=True)
281                 ),
282                 "uuid": dispatched_section_uuid,
283             }
284             for participation in self.shift.participations.filter(
285                 state=AbstractParticipation.States.CONFIRMED
286             )
287             if (dispatched_section_uuid := participation.data.get("dispatched_section_uuid"))
288             and dispatched_section_uuid in section_by_uuid
289         ]
290         # group by section and do some stats
291         sections_with_users = [
292             (
293                 section_by_uuid.pop(uuid),
294                 [[user["name"], user["relevant_qualifications"]] for user in group],
295             )
296             for uuid, group in groupby(
297                 sorted(confirmed_participations, key=itemgetter("uuid")), itemgetter("uuid")
298             )
299         ]
300         # add sections without participants
301         sections_with_users += [(section, None) for section in section_by_uuid.values()]
302         return sections_with_users
303 
304     def render_shift_state(self, request):
305         return get_template("basesignup/section_based/fragment_state.html").render(
306             {
307                 "shift": self.shift,
308                 "requested_participations": (
309                     self.shift.participations.filter(state=AbstractParticipation.States.REQUESTED)
310                 ),
311                 "sections_with_users": self._get_sections_with_users(),
312                 "disposition_url": (
313                     reverse(
314                         "core:shift_disposition",
315                         kwargs=dict(pk=self.shift.pk),
316                     )
317                     if request.user.has_perm("core.change_event", obj=self.shift.event)
318                     else None
319                 ),
320             }
321         )
322 
323     def get_participation_display(self):
324         confirmed_sections_with_users = self._get_sections_with_users()
325         participation_display = []
326         for section, users in confirmed_sections_with_users:
327             if users:
328                 participation_display += [[user[0], user[1], section["title"]] for user in users]
329             if not users or len(users) < section["min_count"]:
330                 required_qualifications = ", ".join(
331                     Qualification.objects.filter(pk__in=section["qualifications"]).values_list(
332                         "abbreviation", flat=True
333                     )
334                 )
335                 participation_display += [["", required_qualifications, section["title"]]] * (
336                     section["min_count"] - (len(users) if users else 0)
337                 )
338         return participation_display
339
```
--- 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/ephios/plugins/basesignup/signup/section_based.py b/ephios/plugins/basesignup/signup/section_based.py
--- a/ephios/plugins/basesignup/signup/section_based.py
+++ b/ephios/plugins/basesignup/signup/section_based.py
@@ -104,6 +104,7 @@
         required=False,
     )
     min_count = forms.IntegerField(label=_("min amount"), min_value=0, required=True)
+    max_count = forms.IntegerField(label=_("max amount"), min_value=1, required=False)
     uuid = forms.CharField(widget=forms.HiddenInput, required=False)
 
     def clean_uuid(self):
@@ -131,7 +132,7 @@
         sections = [
             {
                 key: form.cleaned_data[key]
-                for key in ("title", "qualifications", "min_count", "uuid")
+                for key in ("title", "qualifications", "min_count", "max_count", "uuid")
             }
             for form in self.sections_formset
             if not form.cleaned_data.get("DELETE")
@@ -233,7 +234,15 @@
         }
 
     def get_participant_count_bounds(self):
-        return sum(section.get("min_count") or 0 for section in self.configuration.sections), None
+        return (
+            sum(section.get("min_count") or 0 for section in self.configuration.sections),
+            sum(
+                section.get("max_count") or section.get("min_count")
+                for section in self.configuration.sections
+            )
+            if all(section.get("max_count") for section in self.configuration.sections)
+            else None,
+        )
 
     @staticmethod
     def check_qualification(method, participant):

{"golden_diff": "diff --git a/ephios/plugins/basesignup/signup/section_based.py b/ephios/plugins/basesignup/signup/section_based.py\n--- a/ephios/plugins/basesignup/signup/section_based.py\n+++ b/ephios/plugins/basesignup/signup/section_based.py\n@@ -104,6 +104,7 @@\n         required=False,\n     )\n     min_count = forms.IntegerField(label=_(\"min amount\"), min_value=0, required=True)\n+    max_count = forms.IntegerField(label=_(\"max amount\"), min_value=1, required=False)\n     uuid = forms.CharField(widget=forms.HiddenInput, required=False)\n \n     def clean_uuid(self):\n@@ -131,7 +132,7 @@\n         sections = [\n             {\n                 key: form.cleaned_data[key]\n-                for key in (\"title\", \"qualifications\", \"min_count\", \"uuid\")\n+                for key in (\"title\", \"qualifications\", \"min_count\", \"max_count\", \"uuid\")\n             }\n             for form in self.sections_formset\n             if not form.cleaned_data.get(\"DELETE\")\n@@ -233,7 +234,15 @@\n         }\n \n     def get_participant_count_bounds(self):\n-        return sum(section.get(\"min_count\") or 0 for section in self.configuration.sections), None\n+        return (\n+            sum(section.get(\"min_count\") or 0 for section in self.configuration.sections),\n+            sum(\n+                section.get(\"max_count\") or section.get(\"min_count\")\n+                for section in self.configuration.sections\n+            )\n+            if all(section.get(\"max_count\") for section in self.configuration.sections)\n+            else None,\n+        )\n \n     @staticmethod\n     def check_qualification(method, participant):\n", "issue": "Maximum number of participants for SectionBasedSignup\n\n", "before_files": [{"content": "import uuid\nfrom functools import cached_property\nfrom itertools import groupby\nfrom operator import itemgetter\n\nfrom django import forms\nfrom django.contrib import messages\nfrom django.core.exceptions import ValidationError\nfrom django.shortcuts import redirect\nfrom django.template.loader import get_template\nfrom django.urls import reverse\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views.generic import FormView\nfrom django_select2.forms import Select2MultipleWidget\nfrom dynamic_preferences.registries import global_preferences_registry\n\nfrom ephios.core.models import AbstractParticipation, Qualification\nfrom ephios.core.signup import (\n    AbstractParticipant,\n    BaseDispositionParticipationForm,\n    BaseSignupMethod,\n    BaseSignupView,\n    ParticipationError,\n)\n\n\ndef sections_participant_qualifies_for(sections, participant: AbstractParticipant):\n    available_qualification_ids = set(q.id for q in participant.collect_all_qualifications())\n    return [\n        section\n        for section in sections\n        if set(section[\"qualifications\"]) <= available_qualification_ids\n    ]\n\n\nclass SectionBasedDispositionParticipationForm(BaseDispositionParticipationForm):\n    disposition_participation_template = \"basesignup/section_based/fragment_participant.html\"\n\n    section = forms.ChoiceField(\n        label=_(\"Section\"),\n        required=False,  # only required if participation is confirmed\n        widget=forms.Select(\n            attrs={\"data-show-for-state\": str(AbstractParticipation.States.CONFIRMED)}\n        ),\n    )\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        sections = self.shift.signup_method.configuration.sections\n        qualified_sections = list(\n            sections_participant_qualifies_for(\n                sections,\n                self.instance.participant,\n            )\n        )\n        unqualified_sections = [\n            section for section in sections if section not in qualified_sections\n        ]\n        self.fields[\"section\"].choices = [(\"\", \"---\")]\n        if qualified_sections:\n            self.fields[\"section\"].choices += [\n                (\n                    _(\"qualified\"),\n                    [(section[\"uuid\"], section[\"title\"]) for section in qualified_sections],\n                )\n            ]\n        if unqualified_sections:\n            self.fields[\"section\"].choices += [\n                (\n                    _(\"unqualified\"),\n                    [(section[\"uuid\"], section[\"title\"]) for section in unqualified_sections],\n                )\n            ]\n        if preferred_section_uuid := self.instance.data.get(\"preferred_section_uuid\"):\n            self.fields[\"section\"].initial = preferred_section_uuid\n            self.preferred_section = next(\n                filter(lambda section: section[\"uuid\"] == preferred_section_uuid, sections), None\n            )\n        if initial := self.instance.data.get(\"dispatched_section_uuid\"):\n            self.fields[\"section\"].initial = initial\n\n    def clean(self):\n        super().clean()\n        if (\n            self.cleaned_data[\"state\"] == AbstractParticipation.States.CONFIRMED\n            and not self.cleaned_data[\"section\"]\n        ):\n            self.add_error(\n                \"section\",\n                ValidationError(_(\"You must select a section when confirming a participation.\")),\n            )\n\n    def save(self, commit=True):\n        self.instance.data[\"dispatched_section_uuid\"] = self.cleaned_data[\"section\"]\n        super().save(commit)\n\n\nclass SectionForm(forms.Form):\n    title = forms.CharField(label=_(\"Title\"), required=True)\n    qualifications = forms.ModelMultipleChoiceField(\n        label=_(\"Required Qualifications\"),\n        queryset=Qualification.objects.all(),\n        widget=Select2MultipleWidget,\n        required=False,\n    )\n    min_count = forms.IntegerField(label=_(\"min amount\"), min_value=0, required=True)\n    uuid = forms.CharField(widget=forms.HiddenInput, required=False)\n\n    def clean_uuid(self):\n        return self.cleaned_data.get(\"uuid\") or uuid.uuid4()\n\n\nSectionsFormset = forms.formset_factory(\n    SectionForm, can_delete=True, min_num=1, validate_min=1, extra=0\n)\n\n\nclass SectionBasedConfigurationForm(forms.Form):\n    def __init__(self, data=None, **kwargs):\n        super().__init__(data, **kwargs)\n        self.sections_formset = SectionsFormset(\n            data=data,\n            initial=self.initial.get(\"sections\", list()),\n            prefix=\"sections\",\n        )\n\n    def clean_sections(self):\n        if not self.sections_formset.is_valid():\n            raise ValidationError(_(\"The sections aren't configured correctly.\"))\n\n        sections = [\n            {\n                key: form.cleaned_data[key]\n                for key in (\"title\", \"qualifications\", \"min_count\", \"uuid\")\n            }\n            for form in self.sections_formset\n            if not form.cleaned_data.get(\"DELETE\")\n        ]\n        return sections\n\n\nclass SectionSignupForm(forms.Form):\n    section = forms.ChoiceField(\n        label=_(\"Preferred Section\"),\n        widget=forms.RadioSelect,\n        required=False,\n        # choices are set as (uuid, title) of section\n    )\n\n\nclass SectionBasedSignupView(FormView, BaseSignupView):\n    template_name = \"basesignup/section_based/signup.html\"\n\n    @cached_property\n    def sections_participant_qualifies_for(self):\n        return sections_participant_qualifies_for(\n            self.method.configuration.sections, self.participant\n        )\n\n    def get_form(self, form_class=None):\n        form = SectionSignupForm(self.request.POST)\n        form.fields[\"section\"].choices = [\n            (section[\"uuid\"], section[\"title\"])\n            for section in self.sections_participant_qualifies_for\n        ]\n        return form\n\n    def get_context_data(self, **kwargs):\n        kwargs.setdefault(\"shift\", self.shift)\n        kwargs.setdefault(\n            \"unqualified_sections\",\n            [\n                section[\"title\"]\n                for section in self.method.configuration.sections\n                if section not in self.sections_participant_qualifies_for\n            ],\n        )\n        return super().get_context_data(**kwargs)\n\n    def form_valid(self, form):\n        return super().signup_pressed(preferred_section_uuid=form.cleaned_data.get(\"section\"))\n\n    def signup_pressed(self, **kwargs):\n        if not self.method.configuration.choose_preferred_section:\n            # do straight signup if choosing is not enabled\n            return super().signup_pressed(**kwargs)\n\n        if not self.method.can_sign_up(self.participant):\n            # redirect a misled request\n            messages.warning(self.request, _(\"You can not sign up for this shift.\"))\n            return redirect(self.participant.reverse_event_detail(self.shift.event))\n\n        # all good, redirect to the form\n        return redirect(self.participant.reverse_signup_action(self.shift))\n\n\nclass SectionBasedSignupMethod(BaseSignupMethod):\n    slug = \"section_based\"\n    verbose_name = _(\"Apply for sections\")\n    description = _(\n        \"\"\"This method lets you define sections for which people can choose from.\n        Sections contain qualifications that helpers need to fulfil.\"\"\"\n    )\n    registration_button_text = _(\"Request\")\n    signup_success_message = _(\"You have successfully requested a participation for {shift}.\")\n    signup_error_message = _(\"Requesting a participation failed: {error}\")\n\n    configuration_form_class = SectionBasedConfigurationForm\n    signup_view_class = SectionBasedSignupView\n\n    disposition_participation_form_class = SectionBasedDispositionParticipationForm\n\n    def get_configuration_fields(self):\n        return {\n            **super().get_configuration_fields(),\n            \"choose_preferred_section\": {\n                \"formfield\": forms.BooleanField(\n                    label=_(\"Ask participants for a preferred section\"),\n                    help_text=_(\"This only makes sense if you configure multiple sections.\"),\n                    widget=forms.CheckboxInput,\n                    required=False,\n                ),\n                \"default\": False,\n            },\n            \"sections\": {\n                \"formfield\": forms.Field(\n                    label=_(\"Structure\"),\n                    widget=forms.HiddenInput,\n                    required=False,\n                ),\n                \"default\": [],\n            },\n        }\n\n    def get_participant_count_bounds(self):\n        return sum(section.get(\"min_count\") or 0 for section in self.configuration.sections), None\n\n    @staticmethod\n    def check_qualification(method, participant):\n        if not sections_participant_qualifies_for(method.configuration.sections, participant):\n            return ParticipationError(_(\"You are not qualified.\"))\n\n    @property\n    def _signup_checkers(self):\n        return super()._signup_checkers + [self.check_qualification]\n\n    # pylint: disable=arguments-differ\n    def _configure_participation(\n        self, participation: AbstractParticipation, preferred_section_uuid=None, **kwargs\n    ) -> AbstractParticipation:\n        participation.data[\"preferred_section_uuid\"] = preferred_section_uuid\n        if preferred_section_uuid:\n            # reset dispatch decision, as that would have overwritten the preferred choice\n            participation.data[\"dispatched_section_uuid\"] = None\n        participation.state = AbstractParticipation.States.REQUESTED\n        return participation\n\n    def render_configuration_form(self, *args, form=None, **kwargs):\n        form = form or self.get_configuration_form(*args, **kwargs)\n        template = get_template(\"basesignup/section_based/configuration_form.html\").render(\n            {\"form\": form}\n        )\n        return template\n\n    def _get_sections_with_users(self):\n        relevant_qualification_categories = global_preferences_registry.manager()[\n            \"general__relevant_qualification_categories\"\n        ]\n        section_by_uuid = {section[\"uuid\"]: section for section in self.configuration.sections}\n        # get name and preferred section uuid for confirmed participants\n        # if they have a section assigned and we have that section on record\n        confirmed_participations = [\n            {\n                \"name\": str(participation.participant),\n                \"relevant_qualifications\": \", \".join(\n                    participation.participant.qualifications.filter(\n                        category__in=relevant_qualification_categories\n                    )\n                    .order_by(\"category\", \"abbreviation\")\n                    .values_list(\"abbreviation\", flat=True)\n                ),\n                \"uuid\": dispatched_section_uuid,\n            }\n            for participation in self.shift.participations.filter(\n                state=AbstractParticipation.States.CONFIRMED\n            )\n            if (dispatched_section_uuid := participation.data.get(\"dispatched_section_uuid\"))\n            and dispatched_section_uuid in section_by_uuid\n        ]\n        # group by section and do some stats\n        sections_with_users = [\n            (\n                section_by_uuid.pop(uuid),\n                [[user[\"name\"], user[\"relevant_qualifications\"]] for user in group],\n            )\n            for uuid, group in groupby(\n                sorted(confirmed_participations, key=itemgetter(\"uuid\")), itemgetter(\"uuid\")\n            )\n        ]\n        # add sections without participants\n        sections_with_users += [(section, None) for section in section_by_uuid.values()]\n        return sections_with_users\n\n    def render_shift_state(self, request):\n        return get_template(\"basesignup/section_based/fragment_state.html\").render(\n            {\n                \"shift\": self.shift,\n                \"requested_participations\": (\n                    self.shift.participations.filter(state=AbstractParticipation.States.REQUESTED)\n                ),\n                \"sections_with_users\": self._get_sections_with_users(),\n                \"disposition_url\": (\n                    reverse(\n                        \"core:shift_disposition\",\n                        kwargs=dict(pk=self.shift.pk),\n                    )\n                    if request.user.has_perm(\"core.change_event\", obj=self.shift.event)\n                    else None\n                ),\n            }\n        )\n\n    def get_participation_display(self):\n        confirmed_sections_with_users = self._get_sections_with_users()\n        participation_display = []\n        for section, users in confirmed_sections_with_users:\n            if users:\n                participation_display += [[user[0], user[1], section[\"title\"]] for user in users]\n            if not users or len(users) < section[\"min_count\"]:\n                required_qualifications = \", \".join(\n                    Qualification.objects.filter(pk__in=section[\"qualifications\"]).values_list(\n                        \"abbreviation\", flat=True\n                    )\n                )\n                participation_display += [[\"\", required_qualifications, section[\"title\"]]] * (\n                    section[\"min_count\"] - (len(users) if users else 0)\n                )\n        return participation_display\n", "path": "ephios/plugins/basesignup/signup/section_based.py"}], "after_files": [{"content": "import uuid\nfrom functools import cached_property\nfrom itertools import groupby\nfrom operator import itemgetter\n\nfrom django import forms\nfrom django.contrib import messages\nfrom django.core.exceptions import ValidationError\nfrom django.shortcuts import redirect\nfrom django.template.loader import get_template\nfrom django.urls import reverse\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views.generic import FormView\nfrom django_select2.forms import Select2MultipleWidget\nfrom dynamic_preferences.registries import global_preferences_registry\n\nfrom ephios.core.models import AbstractParticipation, Qualification\nfrom ephios.core.signup import (\n    AbstractParticipant,\n    BaseDispositionParticipationForm,\n    BaseSignupMethod,\n    BaseSignupView,\n    ParticipationError,\n)\n\n\ndef sections_participant_qualifies_for(sections, participant: AbstractParticipant):\n    available_qualification_ids = set(q.id for q in participant.collect_all_qualifications())\n    return [\n        section\n        for section in sections\n        if set(section[\"qualifications\"]) <= available_qualification_ids\n    ]\n\n\nclass SectionBasedDispositionParticipationForm(BaseDispositionParticipationForm):\n    disposition_participation_template = \"basesignup/section_based/fragment_participant.html\"\n\n    section = forms.ChoiceField(\n        label=_(\"Section\"),\n        required=False,  # only required if participation is confirmed\n        widget=forms.Select(\n            attrs={\"data-show-for-state\": str(AbstractParticipation.States.CONFIRMED)}\n        ),\n    )\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        sections = self.shift.signup_method.configuration.sections\n        qualified_sections = list(\n            sections_participant_qualifies_for(\n                sections,\n                self.instance.participant,\n            )\n        )\n        unqualified_sections = [\n            section for section in sections if section not in qualified_sections\n        ]\n        self.fields[\"section\"].choices = [(\"\", \"---\")]\n        if qualified_sections:\n            self.fields[\"section\"].choices += [\n                (\n                    _(\"qualified\"),\n                    [(section[\"uuid\"], section[\"title\"]) for section in qualified_sections],\n                )\n            ]\n        if unqualified_sections:\n            self.fields[\"section\"].choices += [\n                (\n                    _(\"unqualified\"),\n                    [(section[\"uuid\"], section[\"title\"]) for section in unqualified_sections],\n                )\n            ]\n        if preferred_section_uuid := self.instance.data.get(\"preferred_section_uuid\"):\n            self.fields[\"section\"].initial = preferred_section_uuid\n            self.preferred_section = next(\n                filter(lambda section: section[\"uuid\"] == preferred_section_uuid, sections), None\n            )\n        if initial := self.instance.data.get(\"dispatched_section_uuid\"):\n            self.fields[\"section\"].initial = initial\n\n    def clean(self):\n        super().clean()\n        if (\n            self.cleaned_data[\"state\"] == AbstractParticipation.States.CONFIRMED\n            and not self.cleaned_data[\"section\"]\n        ):\n            self.add_error(\n                \"section\",\n                ValidationError(_(\"You must select a section when confirming a participation.\")),\n            )\n\n    def save(self, commit=True):\n        self.instance.data[\"dispatched_section_uuid\"] = self.cleaned_data[\"section\"]\n        super().save(commit)\n\n\nclass SectionForm(forms.Form):\n    title = forms.CharField(label=_(\"Title\"), required=True)\n    qualifications = forms.ModelMultipleChoiceField(\n        label=_(\"Required Qualifications\"),\n        queryset=Qualification.objects.all(),\n        widget=Select2MultipleWidget,\n        required=False,\n    )\n    min_count = forms.IntegerField(label=_(\"min amount\"), min_value=0, required=True)\n    max_count = forms.IntegerField(label=_(\"max amount\"), min_value=1, required=False)\n    uuid = forms.CharField(widget=forms.HiddenInput, required=False)\n\n    def clean_uuid(self):\n        return self.cleaned_data.get(\"uuid\") or uuid.uuid4()\n\n\nSectionsFormset = forms.formset_factory(\n    SectionForm, can_delete=True, min_num=1, validate_min=1, extra=0\n)\n\n\nclass SectionBasedConfigurationForm(forms.Form):\n    def __init__(self, data=None, **kwargs):\n        super().__init__(data, **kwargs)\n        self.sections_formset = SectionsFormset(\n            data=data,\n            initial=self.initial.get(\"sections\", list()),\n            prefix=\"sections\",\n        )\n\n    def clean_sections(self):\n        if not self.sections_formset.is_valid():\n            raise ValidationError(_(\"The sections aren't configured correctly.\"))\n\n        sections = [\n            {\n                key: form.cleaned_data[key]\n                for key in (\"title\", \"qualifications\", \"min_count\", \"max_count\", \"uuid\")\n            }\n            for form in self.sections_formset\n            if not form.cleaned_data.get(\"DELETE\")\n        ]\n        return sections\n\n\nclass SectionSignupForm(forms.Form):\n    section = forms.ChoiceField(\n        label=_(\"Preferred Section\"),\n        widget=forms.RadioSelect,\n        required=False,\n        # choices are set as (uuid, title) of section\n    )\n\n\nclass SectionBasedSignupView(FormView, BaseSignupView):\n    template_name = \"basesignup/section_based/signup.html\"\n\n    @cached_property\n    def sections_participant_qualifies_for(self):\n        return sections_participant_qualifies_for(\n            self.method.configuration.sections, self.participant\n        )\n\n    def get_form(self, form_class=None):\n        form = SectionSignupForm(self.request.POST)\n        form.fields[\"section\"].choices = [\n            (section[\"uuid\"], section[\"title\"])\n            for section in self.sections_participant_qualifies_for\n        ]\n        return form\n\n    def get_context_data(self, **kwargs):\n        kwargs.setdefault(\"shift\", self.shift)\n        kwargs.setdefault(\n            \"unqualified_sections\",\n            [\n                section[\"title\"]\n                for section in self.method.configuration.sections\n                if section not in self.sections_participant_qualifies_for\n            ],\n        )\n        return super().get_context_data(**kwargs)\n\n    def form_valid(self, form):\n        return super().signup_pressed(preferred_section_uuid=form.cleaned_data.get(\"section\"))\n\n    def signup_pressed(self, **kwargs):\n        if not self.method.configuration.choose_preferred_section:\n            # do straight signup if choosing is not enabled\n            return super().signup_pressed(**kwargs)\n\n        if not self.method.can_sign_up(self.participant):\n            # redirect a misled request\n            messages.warning(self.request, _(\"You can not sign up for this shift.\"))\n            return redirect(self.participant.reverse_event_detail(self.shift.event))\n\n        # all good, redirect to the form\n        return redirect(self.participant.reverse_signup_action(self.shift))\n\n\nclass SectionBasedSignupMethod(BaseSignupMethod):\n    slug = \"section_based\"\n    verbose_name = _(\"Apply for sections\")\n    description = _(\n        \"\"\"This method lets you define sections for which people can choose from.\n        Sections contain qualifications that helpers need to fulfil.\"\"\"\n    )\n    registration_button_text = _(\"Request\")\n    signup_success_message = _(\"You have successfully requested a participation for {shift}.\")\n    signup_error_message = _(\"Requesting a participation failed: {error}\")\n\n    configuration_form_class = SectionBasedConfigurationForm\n    signup_view_class = SectionBasedSignupView\n\n    disposition_participation_form_class = SectionBasedDispositionParticipationForm\n\n    def get_configuration_fields(self):\n        return {\n            **super().get_configuration_fields(),\n            \"choose_preferred_section\": {\n                \"formfield\": forms.BooleanField(\n                    label=_(\"Ask participants for a preferred section\"),\n                    help_text=_(\"This only makes sense if you configure multiple sections.\"),\n                    widget=forms.CheckboxInput,\n                    required=False,\n                ),\n                \"default\": False,\n            },\n            \"sections\": {\n                \"formfield\": forms.Field(\n                    label=_(\"Structure\"),\n                    widget=forms.HiddenInput,\n                    required=False,\n                ),\n                \"default\": [],\n            },\n        }\n\n    def get_participant_count_bounds(self):\n        return (\n            sum(section.get(\"min_count\") or 0 for section in self.configuration.sections),\n            sum(\n                section.get(\"max_count\") or section.get(\"min_count\")\n                for section in self.configuration.sections\n            )\n            if all(section.get(\"max_count\") for section in self.configuration.sections)\n            else None,\n        )\n\n    @staticmethod\n    def check_qualification(method, participant):\n        if not sections_participant_qualifies_for(method.configuration.sections, participant):\n            return ParticipationError(_(\"You are not qualified.\"))\n\n    @property\n    def _signup_checkers(self):\n        return super()._signup_checkers + [self.check_qualification]\n\n    # pylint: disable=arguments-differ\n    def _configure_participation(\n        self, participation: AbstractParticipation, preferred_section_uuid=None, **kwargs\n    ) -> AbstractParticipation:\n        participation.data[\"preferred_section_uuid\"] = preferred_section_uuid\n        if preferred_section_uuid:\n            # reset dispatch decision, as that would have overwritten the preferred choice\n            participation.data[\"dispatched_section_uuid\"] = None\n        participation.state = AbstractParticipation.States.REQUESTED\n        return participation\n\n    def render_configuration_form(self, *args, form=None, **kwargs):\n        form = form or self.get_configuration_form(*args, **kwargs)\n        template = get_template(\"basesignup/section_based/configuration_form.html\").render(\n            {\"form\": form}\n        )\n        return template\n\n    def _get_sections_with_users(self):\n        relevant_qualification_categories = global_preferences_registry.manager()[\n            \"general__relevant_qualification_categories\"\n        ]\n        section_by_uuid = {section[\"uuid\"]: section for section in self.configuration.sections}\n        # get name and preferred section uuid for confirmed participants\n        # if they have a section assigned and we have that section on record\n        confirmed_participations = [\n            {\n                \"name\": str(participation.participant),\n                \"relevant_qualifications\": \", \".join(\n                    participation.participant.qualifications.filter(\n                        category__in=relevant_qualification_categories\n                    )\n                    .order_by(\"category\", \"abbreviation\")\n                    .values_list(\"abbreviation\", flat=True)\n                ),\n                \"uuid\": dispatched_section_uuid,\n            }\n            for participation in self.shift.participations.filter(\n                state=AbstractParticipation.States.CONFIRMED\n            )\n            if (dispatched_section_uuid := participation.data.get(\"dispatched_section_uuid\"))\n            and dispatched_section_uuid in section_by_uuid\n        ]\n        # group by section and do some stats\n        sections_with_users = [\n            (\n                section_by_uuid.pop(uuid),\n                [[user[\"name\"], user[\"relevant_qualifications\"]] for user in group],\n            )\n            for uuid, group in groupby(\n                sorted(confirmed_participations, key=itemgetter(\"uuid\")), itemgetter(\"uuid\")\n            )\n        ]\n        # add sections without participants\n        sections_with_users += [(section, None) for section in section_by_uuid.values()]\n        return sections_with_users\n\n    def render_shift_state(self, request):\n        return get_template(\"basesignup/section_based/fragment_state.html\").render(\n            {\n                \"shift\": self.shift,\n                \"requested_participations\": (\n                    self.shift.participations.filter(state=AbstractParticipation.States.REQUESTED)\n                ),\n                \"sections_with_users\": self._get_sections_with_users(),\n                \"disposition_url\": (\n                    reverse(\n                        \"core:shift_disposition\",\n                        kwargs=dict(pk=self.shift.pk),\n                    )\n                    if request.user.has_perm(\"core.change_event\", obj=self.shift.event)\n                    else None\n                ),\n            }\n        )\n\n    def get_participation_display(self):\n        confirmed_sections_with_users = self._get_sections_with_users()\n        participation_display = []\n        for section, users in confirmed_sections_with_users:\n            if users:\n                participation_display += [[user[0], user[1], section[\"title\"]] for user in users]\n            if not users or len(users) < section[\"min_count\"]:\n                required_qualifications = \", \".join(\n                    Qualification.objects.filter(pk__in=section[\"qualifications\"]).values_list(\n                        \"abbreviation\", flat=True\n                    )\n                )\n                participation_display += [[\"\", required_qualifications, section[\"title\"]]] * (\n                    section[\"min_count\"] - (len(users) if users else 0)\n                )\n        return participation_display\n", "path": "ephios/plugins/basesignup/signup/section_based.py"}]}