We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Switching from signup method with requested state to one w/o breaks disposition
```

2021-12-03 14:39:00,166 DEBG 'uwsgi' stderr output:
Traceback (most recent call last):
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner

2021-12-03 14:39:00,167 DEBG 'uwsgi' stderr output:
    response = get_response(request)
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/core/handlers/base.py", line 181, in _get_response

2021-12-03 14:39:00,168 DEBG 'uwsgi' stderr output:
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/views/generic/base.py", line 70, in view

2021-12-03 14:39:00,168 DEBG 'uwsgi' stderr output:
    return self.dispatch(request, *args, **kwargs)
  File "/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/disposition.py", line 118, in dispatch

2021-12-03 14:39:00,169 DEBG 'uwsgi' stderr output:
    return super().dispatch(request, *args, **kwargs)
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/contrib/auth/mixins.py", line 104, in dispatch

2021-12-03 14:39:00,169 DEBG 'uwsgi' stderr output:
    return super().dispatch(request, *args, **kwargs)
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/disposition.py", line 224, in post

2021-12-03 14:39:00,170 DEBG 'uwsgi' stderr output:
    if not formset.is_valid():
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py", line 321, in is_valid

2021-12-03 14:39:00,171 DEBG 'uwsgi' stderr output:
    self.errors
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py", line 304, in errors

2021-12-03 14:39:00,171 DEBG 'uwsgi' stderr output:
    self.full_clean()
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py", line 355, in full_clean

2021-12-03 14:39:00,172 DEBG 'uwsgi' stderr output:
    for i, form in enumerate(self.forms):
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/utils/functional.py", line 48, in __get__

2021-12-03 14:39:00,172 DEBG 'uwsgi' stderr output:
    res = instance.__dict__[self.name] = self.func(instance)
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py", line 155, in forms
    return [
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py", line 156, in <listcomp>

2021-12-03 14:39:00,175 DEBG 'uwsgi' stderr output:
    self._construct_form(i, **self.get_form_kwargs(i))
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/forms/models.py", line 632, in _construct_form
    form = super()._construct_form(i, **kwargs)
  File "/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py", line 193, in _construct_form
    form = self.form(**defaults)
  File "/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/disposition.py", line 31, in __init__
    super().__init__(**kwargs)
  File "/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/methods.py", line 141, in __init__
    instance = kwargs["instance"]
KeyError: 'instance'

```

--- 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/core/signup/disposition.py`
Content:
```
1 from django import forms
2 from django.http import Http404
3 from django.shortcuts import redirect
4 from django.utils.translation import gettext_lazy as _
5 from django.views import View
6 from django.views.generic import TemplateView
7 from django.views.generic.base import TemplateResponseMixin
8 from django.views.generic.detail import SingleObjectMixin
9 from django_select2.forms import ModelSelect2Widget
10 
11 from ephios.core.models import (
12     AbstractParticipation,
13     LocalParticipation,
14     Qualification,
15     Shift,
16     UserProfile,
17 )
18 from ephios.core.services.notifications.types import (
19     ParticipationConfirmedNotification,
20     ParticipationCustomizationNotification,
21     ParticipationRejectedNotification,
22 )
23 from ephios.core.signup.methods import BaseParticipationForm
24 from ephios.extra.mixins import CustomPermissionRequiredMixin
25 
26 
27 class BaseDispositionParticipationForm(BaseParticipationForm):
28     disposition_participation_template = "core/disposition/fragment_participation.html"
29 
30     def __init__(self, **kwargs):
31         super().__init__(**kwargs)
32         self.can_delete = self.instance.state == AbstractParticipation.States.GETTING_DISPATCHED
33         self.fields["comment"].disabled = True
34         try:
35             self.shift = self.instance.shift
36         except AttributeError as e:
37             raise ValueError(f"{type(self)} must be initialized with an instance.") from e
38 
39     class Meta(BaseParticipationForm.Meta):
40         fields = ["state", "individual_start_time", "individual_end_time", "comment"]
41         widgets = dict(state=forms.HiddenInput(attrs={"class": "state-input"}))
42 
43 
44 class DispositionBaseModelFormset(forms.BaseModelFormSet):
45     """
46     To allow us to dynamically add server-side rendered forms to a formset
47     we patch a way to change the starting index.
48     """
49 
50     def __init__(self, *args, start_index=0, **kwargs):
51         self._start_index = start_index
52         super().__init__(*args, **kwargs)
53 
54     def add_prefix(self, index):
55         return f"{self.prefix}-{self._start_index + index}"
56 
57     def save_existing(self, form, instance, commit=True):
58         """Existing participation state overwrites the getting dispatched state."""
59         if form.instance.state == AbstractParticipation.States.GETTING_DISPATCHED:
60             form.instance.state = AbstractParticipation.objects.get(id=form.instance.id).state
61         return form.save(commit=commit)
62 
63 
64 def get_disposition_formset(form):
65     return forms.modelformset_factory(
66         model=AbstractParticipation,
67         formset=DispositionBaseModelFormset,
68         form=form,
69         extra=0,
70         can_order=False,
71         can_delete=True,
72     )
73 
74 
75 def addable_users(shift):
76     """
77     Return queryset of user objects that can be added to the shift.
78     This also includes users that already have a participation, as that might have gotten removed in JS.
79 
80     This also includes users that can normally not see the event. The permission will be added accordingly.
81     If needed, this method could be moved to signup methods.
82     """
83     return UserProfile.objects.all()
84 
85 
86 class AddUserForm(forms.Form):
87     user = forms.ModelChoiceField(
88         widget=ModelSelect2Widget(
89             model=UserProfile,
90             search_fields=["first_name__icontains", "last_name__icontains"],
91             attrs={
92                 "form": "add-user-form",
93                 "data-placeholder": _("search"),
94                 "data-tags": "true",
95                 "data-token-separators": [],
96             },
97         ),
98         queryset=UserProfile.objects.none(),  # set using __init__
99     )
100     new_index = forms.IntegerField(widget=forms.HiddenInput)
101 
102     def __init__(self, user_queryset, *args, **kwargs):
103         super().__init__(*args, **kwargs)
104         self.fields["user"].queryset = user_queryset
105 
106 
107 class DispositionBaseViewMixin(CustomPermissionRequiredMixin, SingleObjectMixin):
108     permission_required = "core.change_event"
109     model = Shift
110 
111     def setup(self, request, *args, **kwargs):
112         super().setup(request, *args, **kwargs)
113         self.object: Shift = self.get_object()
114 
115     def dispatch(self, request, *args, **kwargs):
116         if self.object.signup_method.disposition_participation_form_class is None:
117             raise Http404(_("This signup method does not support disposition."))
118         return super().dispatch(request, *args, **kwargs)
119 
120     def get_permission_object(self):
121         return self.object.event
122 
123 
124 class AddUserView(DispositionBaseViewMixin, TemplateResponseMixin, View):
125     def get_template_names(self):
126         return [
127             self.object.signup_method.disposition_participation_form_class.disposition_participation_template
128         ]
129 
130     def post(self, request, *args, **kwargs):
131         shift = self.object
132         form = AddUserForm(
133             data=request.POST,
134             user_queryset=addable_users(shift),
135         )
136         if form.is_valid():
137             user: UserProfile = form.cleaned_data["user"]
138             instance = shift.signup_method.get_participation_for(user.as_participant())
139             instance.state = AbstractParticipation.States.GETTING_DISPATCHED
140             instance.save()
141 
142             DispositionParticipationFormset = get_disposition_formset(
143                 self.object.signup_method.disposition_participation_form_class
144             )
145             formset = DispositionParticipationFormset(
146                 queryset=AbstractParticipation.objects.filter(pk=instance.pk),
147                 prefix="participations",
148                 start_index=form.cleaned_data["new_index"],
149             )
150             form = next(filter(lambda form: form.instance.id == instance.id, formset))
151             return self.render_to_response({"form": form, "shift": shift})
152         raise Http404()
153 
154 
155 class AddPlaceholderParticipantView(DispositionBaseViewMixin, TemplateResponseMixin, View):
156     def get_template_names(self):
157         return [
158             self.object.signup_method.disposition_participation_form_class.disposition_participation_template
159         ]
160 
161     def post(self, request, *args, **kwargs):
162         shift = self.object
163         from ephios.core.signup.participants import PlaceholderParticipant
164 
165         participant = PlaceholderParticipant(
166             first_name=request.POST["first_name"],
167             last_name=request.POST["last_name"],
168             qualifications=Qualification.objects.none(),
169             email=None,
170             date_of_birth=None,
171         )
172         instance = shift.signup_method.get_participation_for(participant)
173         instance.state = AbstractParticipation.States.GETTING_DISPATCHED
174         instance.save()
175 
176         DispositionParticipationFormset = get_disposition_formset(
177             self.object.signup_method.disposition_participation_form_class
178         )
179         formset = DispositionParticipationFormset(
180             queryset=AbstractParticipation.objects.filter(pk=instance.pk),
181             prefix="participations",
182             start_index=int(request.POST["new_index"]),
183         )
184         form = next(filter(lambda form: form.instance.id == instance.id, formset))
185         return self.render_to_response({"form": form, "shift": shift})
186 
187 
188 class DispositionView(DispositionBaseViewMixin, TemplateView):
189     template_name = "core/disposition/disposition.html"
190 
191     def get_formset(self):
192         DispositionParticipationFormset = get_disposition_formset(
193             self.object.signup_method.disposition_participation_form_class
194         )
195         formset = DispositionParticipationFormset(
196             self.request.POST or None,
197             queryset=self.object.participations.all(),
198             prefix="participations",
199         )
200         return formset
201 
202     def _send_participant_notifications(self, formset):
203         for participation, changed_fields in formset.changed_objects:
204             if (
205                 participation.get_real_instance_class() != LocalParticipation
206                 or participation.user != self.request.user
207             ):
208                 if "state" in changed_fields:
209                     # send state updates
210                     if participation.state == AbstractParticipation.States.CONFIRMED:
211                         ParticipationConfirmedNotification.send(participation)
212                     elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:
213                         ParticipationRejectedNotification.send(participation)
214                 elif participation.state == AbstractParticipation.States.CONFIRMED:
215                     form: BaseParticipationForm = next(
216                         filter(lambda f, p=participation: f.instance == p, formset.forms)
217                     )
218                     if claims := form.get_customization_notification_info():
219                         # If state didn't change, but confirmed participation was customized, notify about that.
220                         ParticipationCustomizationNotification.send(participation, claims)
221 
222     def post(self, request, *args, **kwargs):
223         formset = self.get_formset()
224         if not formset.is_valid():
225             return self.get(request, *args, **kwargs, formset=formset)
226 
227         formset.save()
228         self._send_participant_notifications(formset)
229 
230         self.object.participations.filter(
231             state=AbstractParticipation.States.GETTING_DISPATCHED
232         ).non_polymorphic().delete()
233         return redirect(self.object.event.get_absolute_url())
234 
235     def get_context_data(self, **kwargs):
236         kwargs.setdefault("formset", self.get_formset())
237         kwargs.setdefault("states", AbstractParticipation.States)
238         kwargs.setdefault(
239             "participant_template",
240             self.object.signup_method.disposition_participation_form_class.disposition_participation_template,
241         )
242         kwargs.setdefault(
243             "add_user_form",
244             AddUserForm(user_queryset=addable_users(self.object)),
245         )
246         return super().get_context_data(**kwargs)
247
```
--- 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/core/signup/disposition.py b/ephios/core/signup/disposition.py
--- a/ephios/core/signup/disposition.py
+++ b/ephios/core/signup/disposition.py
@@ -239,6 +239,13 @@
             "participant_template",
             self.object.signup_method.disposition_participation_form_class.disposition_participation_template,
         )
+        kwargs.setdefault(
+            "render_requested_state",
+            self.object.signup_method.uses_requested_state
+            or self.object.participations.filter(
+                state=AbstractParticipation.States.REQUESTED
+            ).exists(),
+        )
         kwargs.setdefault(
             "add_user_form",
             AddUserForm(user_queryset=addable_users(self.object)),

{"golden_diff": "diff --git a/ephios/core/signup/disposition.py b/ephios/core/signup/disposition.py\n--- a/ephios/core/signup/disposition.py\n+++ b/ephios/core/signup/disposition.py\n@@ -239,6 +239,13 @@\n             \"participant_template\",\n             self.object.signup_method.disposition_participation_form_class.disposition_participation_template,\n         )\n+        kwargs.setdefault(\n+            \"render_requested_state\",\n+            self.object.signup_method.uses_requested_state\n+            or self.object.participations.filter(\n+                state=AbstractParticipation.States.REQUESTED\n+            ).exists(),\n+        )\n         kwargs.setdefault(\n             \"add_user_form\",\n             AddUserForm(user_queryset=addable_users(self.object)),\n", "issue": "Switching from signup method with requested state to one w/o breaks disposition\n```\r\n\r\n2021-12-03 14:39:00,166 DEBG 'uwsgi' stderr output:\r\nTraceback (most recent call last):\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/core/handlers/exception.py\", line 47, in inner\r\n\r\n2021-12-03 14:39:00,167 DEBG 'uwsgi' stderr output:\r\n    response = get_response(request)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/core/handlers/base.py\", line 181, in _get_response\r\n\r\n2021-12-03 14:39:00,168 DEBG 'uwsgi' stderr output:\r\n    response = wrapped_callback(request, *callback_args, **callback_kwargs)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/views/generic/base.py\", line 70, in view\r\n\r\n2021-12-03 14:39:00,168 DEBG 'uwsgi' stderr output:\r\n    return self.dispatch(request, *args, **kwargs)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/disposition.py\", line 118, in dispatch\r\n\r\n2021-12-03 14:39:00,169 DEBG 'uwsgi' stderr output:\r\n    return super().dispatch(request, *args, **kwargs)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/contrib/auth/mixins.py\", line 104, in dispatch\r\n\r\n2021-12-03 14:39:00,169 DEBG 'uwsgi' stderr output:\r\n    return super().dispatch(request, *args, **kwargs)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/views/generic/base.py\", line 98, in dispatch\r\n    return handler(request, *args, **kwargs)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/disposition.py\", line 224, in post\r\n\r\n2021-12-03 14:39:00,170 DEBG 'uwsgi' stderr output:\r\n    if not formset.is_valid():\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py\", line 321, in is_valid\r\n\r\n2021-12-03 14:39:00,171 DEBG 'uwsgi' stderr output:\r\n    self.errors\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py\", line 304, in errors\r\n\r\n2021-12-03 14:39:00,171 DEBG 'uwsgi' stderr output:\r\n    self.full_clean()\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py\", line 355, in full_clean\r\n\r\n2021-12-03 14:39:00,172 DEBG 'uwsgi' stderr output:\r\n    for i, form in enumerate(self.forms):\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/utils/functional.py\", line 48, in __get__\r\n\r\n2021-12-03 14:39:00,172 DEBG 'uwsgi' stderr output:\r\n    res = instance.__dict__[self.name] = self.func(instance)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py\", line 155, in forms\r\n    return [\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py\", line 156, in <listcomp>\r\n\r\n2021-12-03 14:39:00,175 DEBG 'uwsgi' stderr output:\r\n    self._construct_form(i, **self.get_form_kwargs(i))\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/forms/models.py\", line 632, in _construct_form\r\n    form = super()._construct_form(i, **kwargs)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/django/forms/formsets.py\", line 193, in _construct_form\r\n    form = self.form(**defaults)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/disposition.py\", line 31, in __init__\r\n    super().__init__(**kwargs)\r\n  File \"/home/juhsp/.local/lib/python3.8/site-packages/ephios/core/signup/methods.py\", line 141, in __init__\r\n    instance = kwargs[\"instance\"]\r\nKeyError: 'instance'\r\n\r\n```\n", "before_files": [{"content": "from django import forms\nfrom django.http import Http404\nfrom django.shortcuts import redirect\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\nfrom django.views.generic import TemplateView\nfrom django.views.generic.base import TemplateResponseMixin\nfrom django.views.generic.detail import SingleObjectMixin\nfrom django_select2.forms import ModelSelect2Widget\n\nfrom ephios.core.models import (\n    AbstractParticipation,\n    LocalParticipation,\n    Qualification,\n    Shift,\n    UserProfile,\n)\nfrom ephios.core.services.notifications.types import (\n    ParticipationConfirmedNotification,\n    ParticipationCustomizationNotification,\n    ParticipationRejectedNotification,\n)\nfrom ephios.core.signup.methods import BaseParticipationForm\nfrom ephios.extra.mixins import CustomPermissionRequiredMixin\n\n\nclass BaseDispositionParticipationForm(BaseParticipationForm):\n    disposition_participation_template = \"core/disposition/fragment_participation.html\"\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.can_delete = self.instance.state == AbstractParticipation.States.GETTING_DISPATCHED\n        self.fields[\"comment\"].disabled = True\n        try:\n            self.shift = self.instance.shift\n        except AttributeError as e:\n            raise ValueError(f\"{type(self)} must be initialized with an instance.\") from e\n\n    class Meta(BaseParticipationForm.Meta):\n        fields = [\"state\", \"individual_start_time\", \"individual_end_time\", \"comment\"]\n        widgets = dict(state=forms.HiddenInput(attrs={\"class\": \"state-input\"}))\n\n\nclass DispositionBaseModelFormset(forms.BaseModelFormSet):\n    \"\"\"\n    To allow us to dynamically add server-side rendered forms to a formset\n    we patch a way to change the starting index.\n    \"\"\"\n\n    def __init__(self, *args, start_index=0, **kwargs):\n        self._start_index = start_index\n        super().__init__(*args, **kwargs)\n\n    def add_prefix(self, index):\n        return f\"{self.prefix}-{self._start_index + index}\"\n\n    def save_existing(self, form, instance, commit=True):\n        \"\"\"Existing participation state overwrites the getting dispatched state.\"\"\"\n        if form.instance.state == AbstractParticipation.States.GETTING_DISPATCHED:\n            form.instance.state = AbstractParticipation.objects.get(id=form.instance.id).state\n        return form.save(commit=commit)\n\n\ndef get_disposition_formset(form):\n    return forms.modelformset_factory(\n        model=AbstractParticipation,\n        formset=DispositionBaseModelFormset,\n        form=form,\n        extra=0,\n        can_order=False,\n        can_delete=True,\n    )\n\n\ndef addable_users(shift):\n    \"\"\"\n    Return queryset of user objects that can be added to the shift.\n    This also includes users that already have a participation, as that might have gotten removed in JS.\n\n    This also includes users that can normally not see the event. The permission will be added accordingly.\n    If needed, this method could be moved to signup methods.\n    \"\"\"\n    return UserProfile.objects.all()\n\n\nclass AddUserForm(forms.Form):\n    user = forms.ModelChoiceField(\n        widget=ModelSelect2Widget(\n            model=UserProfile,\n            search_fields=[\"first_name__icontains\", \"last_name__icontains\"],\n            attrs={\n                \"form\": \"add-user-form\",\n                \"data-placeholder\": _(\"search\"),\n                \"data-tags\": \"true\",\n                \"data-token-separators\": [],\n            },\n        ),\n        queryset=UserProfile.objects.none(),  # set using __init__\n    )\n    new_index = forms.IntegerField(widget=forms.HiddenInput)\n\n    def __init__(self, user_queryset, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.fields[\"user\"].queryset = user_queryset\n\n\nclass DispositionBaseViewMixin(CustomPermissionRequiredMixin, SingleObjectMixin):\n    permission_required = \"core.change_event\"\n    model = Shift\n\n    def setup(self, request, *args, **kwargs):\n        super().setup(request, *args, **kwargs)\n        self.object: Shift = self.get_object()\n\n    def dispatch(self, request, *args, **kwargs):\n        if self.object.signup_method.disposition_participation_form_class is None:\n            raise Http404(_(\"This signup method does not support disposition.\"))\n        return super().dispatch(request, *args, **kwargs)\n\n    def get_permission_object(self):\n        return self.object.event\n\n\nclass AddUserView(DispositionBaseViewMixin, TemplateResponseMixin, View):\n    def get_template_names(self):\n        return [\n            self.object.signup_method.disposition_participation_form_class.disposition_participation_template\n        ]\n\n    def post(self, request, *args, **kwargs):\n        shift = self.object\n        form = AddUserForm(\n            data=request.POST,\n            user_queryset=addable_users(shift),\n        )\n        if form.is_valid():\n            user: UserProfile = form.cleaned_data[\"user\"]\n            instance = shift.signup_method.get_participation_for(user.as_participant())\n            instance.state = AbstractParticipation.States.GETTING_DISPATCHED\n            instance.save()\n\n            DispositionParticipationFormset = get_disposition_formset(\n                self.object.signup_method.disposition_participation_form_class\n            )\n            formset = DispositionParticipationFormset(\n                queryset=AbstractParticipation.objects.filter(pk=instance.pk),\n                prefix=\"participations\",\n                start_index=form.cleaned_data[\"new_index\"],\n            )\n            form = next(filter(lambda form: form.instance.id == instance.id, formset))\n            return self.render_to_response({\"form\": form, \"shift\": shift})\n        raise Http404()\n\n\nclass AddPlaceholderParticipantView(DispositionBaseViewMixin, TemplateResponseMixin, View):\n    def get_template_names(self):\n        return [\n            self.object.signup_method.disposition_participation_form_class.disposition_participation_template\n        ]\n\n    def post(self, request, *args, **kwargs):\n        shift = self.object\n        from ephios.core.signup.participants import PlaceholderParticipant\n\n        participant = PlaceholderParticipant(\n            first_name=request.POST[\"first_name\"],\n            last_name=request.POST[\"last_name\"],\n            qualifications=Qualification.objects.none(),\n            email=None,\n            date_of_birth=None,\n        )\n        instance = shift.signup_method.get_participation_for(participant)\n        instance.state = AbstractParticipation.States.GETTING_DISPATCHED\n        instance.save()\n\n        DispositionParticipationFormset = get_disposition_formset(\n            self.object.signup_method.disposition_participation_form_class\n        )\n        formset = DispositionParticipationFormset(\n            queryset=AbstractParticipation.objects.filter(pk=instance.pk),\n            prefix=\"participations\",\n            start_index=int(request.POST[\"new_index\"]),\n        )\n        form = next(filter(lambda form: form.instance.id == instance.id, formset))\n        return self.render_to_response({\"form\": form, \"shift\": shift})\n\n\nclass DispositionView(DispositionBaseViewMixin, TemplateView):\n    template_name = \"core/disposition/disposition.html\"\n\n    def get_formset(self):\n        DispositionParticipationFormset = get_disposition_formset(\n            self.object.signup_method.disposition_participation_form_class\n        )\n        formset = DispositionParticipationFormset(\n            self.request.POST or None,\n            queryset=self.object.participations.all(),\n            prefix=\"participations\",\n        )\n        return formset\n\n    def _send_participant_notifications(self, formset):\n        for participation, changed_fields in formset.changed_objects:\n            if (\n                participation.get_real_instance_class() != LocalParticipation\n                or participation.user != self.request.user\n            ):\n                if \"state\" in changed_fields:\n                    # send state updates\n                    if participation.state == AbstractParticipation.States.CONFIRMED:\n                        ParticipationConfirmedNotification.send(participation)\n                    elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:\n                        ParticipationRejectedNotification.send(participation)\n                elif participation.state == AbstractParticipation.States.CONFIRMED:\n                    form: BaseParticipationForm = next(\n                        filter(lambda f, p=participation: f.instance == p, formset.forms)\n                    )\n                    if claims := form.get_customization_notification_info():\n                        # If state didn't change, but confirmed participation was customized, notify about that.\n                        ParticipationCustomizationNotification.send(participation, claims)\n\n    def post(self, request, *args, **kwargs):\n        formset = self.get_formset()\n        if not formset.is_valid():\n            return self.get(request, *args, **kwargs, formset=formset)\n\n        formset.save()\n        self._send_participant_notifications(formset)\n\n        self.object.participations.filter(\n            state=AbstractParticipation.States.GETTING_DISPATCHED\n        ).non_polymorphic().delete()\n        return redirect(self.object.event.get_absolute_url())\n\n    def get_context_data(self, **kwargs):\n        kwargs.setdefault(\"formset\", self.get_formset())\n        kwargs.setdefault(\"states\", AbstractParticipation.States)\n        kwargs.setdefault(\n            \"participant_template\",\n            self.object.signup_method.disposition_participation_form_class.disposition_participation_template,\n        )\n        kwargs.setdefault(\n            \"add_user_form\",\n            AddUserForm(user_queryset=addable_users(self.object)),\n        )\n        return super().get_context_data(**kwargs)\n", "path": "ephios/core/signup/disposition.py"}], "after_files": [{"content": "from django import forms\nfrom django.http import Http404\nfrom django.shortcuts import redirect\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views import View\nfrom django.views.generic import TemplateView\nfrom django.views.generic.base import TemplateResponseMixin\nfrom django.views.generic.detail import SingleObjectMixin\nfrom django_select2.forms import ModelSelect2Widget\n\nfrom ephios.core.models import (\n    AbstractParticipation,\n    LocalParticipation,\n    Qualification,\n    Shift,\n    UserProfile,\n)\nfrom ephios.core.services.notifications.types import (\n    ParticipationConfirmedNotification,\n    ParticipationCustomizationNotification,\n    ParticipationRejectedNotification,\n)\nfrom ephios.core.signup.methods import BaseParticipationForm\nfrom ephios.extra.mixins import CustomPermissionRequiredMixin\n\n\nclass BaseDispositionParticipationForm(BaseParticipationForm):\n    disposition_participation_template = \"core/disposition/fragment_participation.html\"\n\n    def __init__(self, **kwargs):\n        super().__init__(**kwargs)\n        self.can_delete = self.instance.state == AbstractParticipation.States.GETTING_DISPATCHED\n        self.fields[\"comment\"].disabled = True\n        try:\n            self.shift = self.instance.shift\n        except AttributeError as e:\n            raise ValueError(f\"{type(self)} must be initialized with an instance.\") from e\n\n    class Meta(BaseParticipationForm.Meta):\n        fields = [\"state\", \"individual_start_time\", \"individual_end_time\", \"comment\"]\n        widgets = dict(state=forms.HiddenInput(attrs={\"class\": \"state-input\"}))\n\n\nclass DispositionBaseModelFormset(forms.BaseModelFormSet):\n    \"\"\"\n    To allow us to dynamically add server-side rendered forms to a formset\n    we patch a way to change the starting index.\n    \"\"\"\n\n    def __init__(self, *args, start_index=0, **kwargs):\n        self._start_index = start_index\n        super().__init__(*args, **kwargs)\n\n    def add_prefix(self, index):\n        return f\"{self.prefix}-{self._start_index + index}\"\n\n    def save_existing(self, form, instance, commit=True):\n        \"\"\"Existing participation state overwrites the getting dispatched state.\"\"\"\n        if form.instance.state == AbstractParticipation.States.GETTING_DISPATCHED:\n            form.instance.state = AbstractParticipation.objects.get(id=form.instance.id).state\n        return form.save(commit=commit)\n\n\ndef get_disposition_formset(form):\n    return forms.modelformset_factory(\n        model=AbstractParticipation,\n        formset=DispositionBaseModelFormset,\n        form=form,\n        extra=0,\n        can_order=False,\n        can_delete=True,\n    )\n\n\ndef addable_users(shift):\n    \"\"\"\n    Return queryset of user objects that can be added to the shift.\n    This also includes users that already have a participation, as that might have gotten removed in JS.\n\n    This also includes users that can normally not see the event. The permission will be added accordingly.\n    If needed, this method could be moved to signup methods.\n    \"\"\"\n    return UserProfile.objects.all()\n\n\nclass AddUserForm(forms.Form):\n    user = forms.ModelChoiceField(\n        widget=ModelSelect2Widget(\n            model=UserProfile,\n            search_fields=[\"first_name__icontains\", \"last_name__icontains\"],\n            attrs={\n                \"form\": \"add-user-form\",\n                \"data-placeholder\": _(\"search\"),\n                \"data-tags\": \"true\",\n                \"data-token-separators\": [],\n            },\n        ),\n        queryset=UserProfile.objects.none(),  # set using __init__\n    )\n    new_index = forms.IntegerField(widget=forms.HiddenInput)\n\n    def __init__(self, user_queryset, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.fields[\"user\"].queryset = user_queryset\n\n\nclass DispositionBaseViewMixin(CustomPermissionRequiredMixin, SingleObjectMixin):\n    permission_required = \"core.change_event\"\n    model = Shift\n\n    def setup(self, request, *args, **kwargs):\n        super().setup(request, *args, **kwargs)\n        self.object: Shift = self.get_object()\n\n    def dispatch(self, request, *args, **kwargs):\n        if self.object.signup_method.disposition_participation_form_class is None:\n            raise Http404(_(\"This signup method does not support disposition.\"))\n        return super().dispatch(request, *args, **kwargs)\n\n    def get_permission_object(self):\n        return self.object.event\n\n\nclass AddUserView(DispositionBaseViewMixin, TemplateResponseMixin, View):\n    def get_template_names(self):\n        return [\n            self.object.signup_method.disposition_participation_form_class.disposition_participation_template\n        ]\n\n    def post(self, request, *args, **kwargs):\n        shift = self.object\n        form = AddUserForm(\n            data=request.POST,\n            user_queryset=addable_users(shift),\n        )\n        if form.is_valid():\n            user: UserProfile = form.cleaned_data[\"user\"]\n            instance = shift.signup_method.get_participation_for(user.as_participant())\n            instance.state = AbstractParticipation.States.GETTING_DISPATCHED\n            instance.save()\n\n            DispositionParticipationFormset = get_disposition_formset(\n                self.object.signup_method.disposition_participation_form_class\n            )\n            formset = DispositionParticipationFormset(\n                queryset=AbstractParticipation.objects.filter(pk=instance.pk),\n                prefix=\"participations\",\n                start_index=form.cleaned_data[\"new_index\"],\n            )\n            form = next(filter(lambda form: form.instance.id == instance.id, formset))\n            return self.render_to_response({\"form\": form, \"shift\": shift})\n        raise Http404()\n\n\nclass AddPlaceholderParticipantView(DispositionBaseViewMixin, TemplateResponseMixin, View):\n    def get_template_names(self):\n        return [\n            self.object.signup_method.disposition_participation_form_class.disposition_participation_template\n        ]\n\n    def post(self, request, *args, **kwargs):\n        shift = self.object\n        from ephios.core.signup.participants import PlaceholderParticipant\n\n        participant = PlaceholderParticipant(\n            first_name=request.POST[\"first_name\"],\n            last_name=request.POST[\"last_name\"],\n            qualifications=Qualification.objects.none(),\n            email=None,\n            date_of_birth=None,\n        )\n        instance = shift.signup_method.get_participation_for(participant)\n        instance.state = AbstractParticipation.States.GETTING_DISPATCHED\n        instance.save()\n\n        DispositionParticipationFormset = get_disposition_formset(\n            self.object.signup_method.disposition_participation_form_class\n        )\n        formset = DispositionParticipationFormset(\n            queryset=AbstractParticipation.objects.filter(pk=instance.pk),\n            prefix=\"participations\",\n            start_index=int(request.POST[\"new_index\"]),\n        )\n        form = next(filter(lambda form: form.instance.id == instance.id, formset))\n        return self.render_to_response({\"form\": form, \"shift\": shift})\n\n\nclass DispositionView(DispositionBaseViewMixin, TemplateView):\n    template_name = \"core/disposition/disposition.html\"\n\n    def get_formset(self):\n        DispositionParticipationFormset = get_disposition_formset(\n            self.object.signup_method.disposition_participation_form_class\n        )\n        formset = DispositionParticipationFormset(\n            self.request.POST or None,\n            queryset=self.object.participations.all(),\n            prefix=\"participations\",\n        )\n        return formset\n\n    def _send_participant_notifications(self, formset):\n        for participation, changed_fields in formset.changed_objects:\n            if (\n                participation.get_real_instance_class() != LocalParticipation\n                or participation.user != self.request.user\n            ):\n                if \"state\" in changed_fields:\n                    # send state updates\n                    if participation.state == AbstractParticipation.States.CONFIRMED:\n                        ParticipationConfirmedNotification.send(participation)\n                    elif participation.state == AbstractParticipation.States.RESPONSIBLE_REJECTED:\n                        ParticipationRejectedNotification.send(participation)\n                elif participation.state == AbstractParticipation.States.CONFIRMED:\n                    form: BaseParticipationForm = next(\n                        filter(lambda f, p=participation: f.instance == p, formset.forms)\n                    )\n                    if claims := form.get_customization_notification_info():\n                        # If state didn't change, but confirmed participation was customized, notify about that.\n                        ParticipationCustomizationNotification.send(participation, claims)\n\n    def post(self, request, *args, **kwargs):\n        formset = self.get_formset()\n        if not formset.is_valid():\n            return self.get(request, *args, **kwargs, formset=formset)\n\n        formset.save()\n        self._send_participant_notifications(formset)\n\n        self.object.participations.filter(\n            state=AbstractParticipation.States.GETTING_DISPATCHED\n        ).non_polymorphic().delete()\n        return redirect(self.object.event.get_absolute_url())\n\n    def get_context_data(self, **kwargs):\n        kwargs.setdefault(\"formset\", self.get_formset())\n        kwargs.setdefault(\"states\", AbstractParticipation.States)\n        kwargs.setdefault(\n            \"participant_template\",\n            self.object.signup_method.disposition_participation_form_class.disposition_participation_template,\n        )\n        kwargs.setdefault(\n            \"render_requested_state\",\n            self.object.signup_method.uses_requested_state\n            or self.object.participations.filter(\n                state=AbstractParticipation.States.REQUESTED\n            ).exists(),\n        )\n        kwargs.setdefault(\n            \"add_user_form\",\n            AddUserForm(user_queryset=addable_users(self.object)),\n        )\n        return super().get_context_data(**kwargs)\n", "path": "ephios/core/signup/disposition.py"}]}