We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
The Cancel button in Add Deadline Deviations behaves surprisingly
Just a detail:

If I go to a student’s page, there are now links ther to add deadline deviations for that particular student. (This is super-useful.) 

Now say I click one of those buttons but then wish to cancel. There’s the cancel button there on the add deviations page, but that doesn’t actually cancel my action in the sense of taking me back where I was. It instead sends me to the list of all deviations (which page, in the case of the O1 course, either crashes with a Bad Gateway or takes approximately forever to load). 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `deviations/viewbase.py`
Content:
```
1 from itertools import groupby
2 from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type
3 
4 from django.db import models
5 from django.http import HttpRequest, HttpResponse
6 from django.contrib import messages
7 from django import forms
8 from django.utils.text import format_lazy
9 from django.utils.translation import gettext_lazy as _, ngettext
10 
11 from course.models import CourseModule, UserTag
12 from course.viewbase import CourseInstanceMixin, CourseInstanceBaseView
13 from deviations.models import SubmissionRuleDeviation
14 from lib.helpers import is_ajax
15 from lib.viewbase import BaseFormView, BaseRedirectView
16 from authorization.permissions import ACCESS
17 from exercise.models import BaseExercise
18 from userprofile.models import UserProfile
19 
20 
21 class ListDeviationsView(CourseInstanceBaseView):
22     access_mode = ACCESS.TEACHER
23     deviation_model: Type[SubmissionRuleDeviation]
24 
25     def get_common_objects(self) -> None:
26         super().get_common_objects()
27         all_deviations = self.deviation_model.objects.filter(
28             exercise__course_module__course_instance=self.instance
29         )
30         self.deviation_groups = get_deviation_groups(all_deviations)
31         self.note("deviation_groups")
32 
33 
34 class AddDeviationsView(CourseInstanceMixin, BaseFormView):
35     access_mode = ACCESS.TEACHER
36     deviation_model: Type[SubmissionRuleDeviation]
37     session_key: str
38 
39     def get_form_kwargs(self) -> Dict[str, Any]:
40         kwargs = super().get_form_kwargs()
41         kwargs["instance"] = self.instance
42         return kwargs
43 
44     def get_initial_get_param_spec(self) -> Dict[str, Optional[Callable[[str], Any]]]:
45         def list_arg(arg):
46             return arg.split(",")
47 
48         spec = super().get_initial_get_param_spec()
49         spec.update({
50             "module": list_arg,
51             "exercise": list_arg,
52             "submitter": list_arg,
53             "submitter_tag": list_arg,
54         })
55         return spec
56 
57     def form_valid(self, form: forms.BaseForm) -> HttpResponse:
58         exercises = get_exercises(form.cleaned_data)
59         submitters = get_submitters(form.cleaned_data)
60         existing_deviations = self.deviation_model.objects.filter(
61             exercise__in=exercises,
62             submitter__in=submitters,
63         )
64 
65         if existing_deviations:
66             # Some deviations already existed. Use OverrideDeviationsView to
67             # confirm which ones the user wants to override. Store the form
68             # values in the current session, so they can be used afterwards.
69             self.success_url = self.deviation_model.get_override_url(self.instance)
70             self.request.session[self.session_key] = self.serialize_session_data(form.cleaned_data)
71         else:
72             self.success_url = self.deviation_model.get_list_url(self.instance)
73             for exercise in exercises:
74                 for submitter in submitters:
75                     new_deviation = self.deviation_model(
76                         exercise=exercise,
77                         submitter=submitter,
78                         granter=self.request.user.userprofile,
79                     )
80                     new_deviation.update_by_form(form.cleaned_data)
81                     new_deviation.save()
82 
83         return super().form_valid(form)
84 
85     def serialize_session_data(self, form_data: Dict[str, Any]) -> Dict[str, Any]:
86         """
87         Convert input form data into serializable values that can be stored in
88         the session cache.
89         """
90         result = {}
91         for key in ('exercise', 'module', 'submitter', 'submitter_tag'):
92             result[key] = [i.id for i in form_data.get(key, [])]
93         return result
94 
95 
96 class OverrideDeviationsView(CourseInstanceMixin, BaseFormView):
97     access_mode = ACCESS.TEACHER
98     # form_class is not really used, but it is required by the FormView.
99     # The form contains only checkboxes and the user input is validated in
100     # the form_valid method. The form HTML is manually written in the template.
101     form_class = forms.Form
102     deviation_model: Type[SubmissionRuleDeviation]
103     session_key: str
104 
105     def get_success_url(self) -> str:
106         return self.deviation_model.get_list_url(self.instance)
107 
108     def get_common_objects(self) -> None:
109         super().get_common_objects()
110         self.session_data = self.deserialize_session_data(self.request.session[self.session_key])
111         self.exercises = get_exercises(self.session_data)
112         self.submitters = get_submitters(self.session_data)
113         self.existing_deviations = self.deviation_model.objects.filter(
114             exercise__in=self.exercises,
115             submitter__in=self.submitters,
116         )
117         self.deviation_groups = get_deviation_groups(self.existing_deviations)
118         self.note("session_data", "exercises", "submitters", "existing_deviations", "deviation_groups")
119 
120     def form_valid(self, form: forms.BaseForm) -> HttpResponse:
121         override_deviations = set()
122         deviation_list = self.request.POST.getlist('override')
123         for id_pair in deviation_list:
124             try:
125                 submitter_id, exercise_id = id_pair.split('.')
126                 submitter_id, exercise_id = int(submitter_id), int(exercise_id)
127                 override_deviations.add((submitter_id, exercise_id))
128             except ValueError:
129                 messages.error(self.request,
130                     format_lazy(
131                         _("INVALID_EXERCISE_OR_SUBMITTER_ID -- {id}"),
132                         id=id_pair,
133                     )
134                 )
135                 continue
136 
137         existing_deviations = {(d.submitter_id, d.exercise_id): d for d in self.existing_deviations}
138 
139         for exercise in self.exercises:
140             for submitter in self.submitters:
141                 existing_deviation = existing_deviations.get((submitter.id, exercise.id))
142                 if existing_deviation is not None:
143                     if (submitter.id, exercise.id) in override_deviations:
144                         existing_deviation.granter = self.request.user.userprofile
145                         existing_deviation.update_by_form(self.session_data)
146                         existing_deviation.save()
147                 else:
148                     new_deviation = self.deviation_model(
149                         exercise=exercise,
150                         submitter=submitter,
151                         granter=self.request.user.userprofile,
152                     )
153                     new_deviation.update_by_form(self.session_data)
154                     new_deviation.save()
155 
156         del self.request.session[self.session_key]
157         return super().form_valid(form)
158 
159     def deserialize_session_data(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
160         """
161         Convert serialized session data back into its original representation.
162         """
163         result = {
164             'exercise': BaseExercise.objects.filter(id__in=session_data.get('exercise', [])),
165             'module': CourseModule.objects.filter(id__in=session_data.get('module', [])),
166             'submitter': UserProfile.objects.filter(id__in=session_data.get('submitter', [])),
167             'submitter_tag': UserTag.objects.filter(id__in=session_data.get('submitter_tag', [])),
168         }
169         return result
170 
171 
172 class RemoveDeviationsByIDView(CourseInstanceMixin, BaseRedirectView):
173     access_mode = ACCESS.TEACHER
174     deviation_model: Type[SubmissionRuleDeviation]
175 
176     def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
177         deviations = self.deviation_model.objects.filter(
178             id__in=request.POST.getlist("id"),
179             exercise__course_module__course_instance=self.instance,
180         )
181         for deviation in deviations:
182             deviation.delete()
183         if is_ajax(request):
184             return HttpResponse(status=204)
185         return self.redirect(self.deviation_model.get_list_url(self.instance))
186 
187 
188 class RemoveDeviationsView(CourseInstanceMixin, BaseFormView):
189     access_mode = ACCESS.TEACHER
190     deviation_model: Type[SubmissionRuleDeviation]
191 
192     def get_form_kwargs(self) -> Dict[str, Any]:
193         kwargs = super().get_form_kwargs()
194         kwargs["instance"] = self.instance
195         return kwargs
196 
197     def get_success_url(self) -> str:
198         return self.deviation_model.get_list_url(self.instance)
199 
200     def form_valid(self, form: forms.BaseForm) -> HttpResponse:
201         number_of_removed = 0
202         deviations = self.deviation_model.objects.filter(
203             exercise__in=get_exercises(form.cleaned_data),
204             submitter__in=get_submitters(form.cleaned_data),
205         )
206         for deviation in deviations:
207             deviation.delete()
208             number_of_removed += 1
209         if number_of_removed == 0:
210             messages.warning(self.request, _("NOTHING_REMOVED"))
211         else:
212             message = ngettext(
213                 'REMOVED_DEVIATION -- {count}',
214                 'REMOVED_DEVIATIONS -- {count}',
215                 number_of_removed,
216             ).format(count=number_of_removed)
217             messages.info(self.request, message)
218         return super().form_valid(form)
219 
220 
221 # pylint: disable-next=too-many-locals
222 def get_deviation_groups(
223         all_deviations: models.QuerySet[SubmissionRuleDeviation],
224         ) -> Iterable[Tuple[List[SubmissionRuleDeviation], bool, Optional[str]]]:
225     """
226     Group the deviations by user and module.
227 
228     Grouping condition: deviations can be grouped if the user has been
229     granted the same deviation (based on the `is_equal` method) for all
230     exercises in the module.
231 
232     The returned tuples contain the following values:
233     1. List of deviations with the same user and module.
234     2. Boolean representing whether the deviations in the list can be
235     displayed as a group (i.e. the grouping condition is satisfied).
236     3. An id that uniquely identifies the group of deviations.
237     """
238     # Find the number of exercises in each module.
239     course_instances = (
240         all_deviations
241         .values_list('exercise__course_module__course_instance', flat=True)
242         .distinct()
243     )
244     exercise_counts = (
245         BaseExercise.objects.filter(
246             course_module__course_instance__in=course_instances
247         )
248         .order_by()
249         .values('course_module_id')
250         .annotate(count=models.Count('*'))
251     )
252     exercise_count_by_module = {row['course_module_id']: row['count'] for row in exercise_counts}
253 
254     ordered_deviations = (
255         all_deviations
256         .select_related(
257             'submitter', 'submitter__user',
258             'granter', 'granter__user',
259             'exercise', 'exercise__course_module',
260             'exercise__course_module__course_instance',
261         )
262         .defer(
263             'exercise__exercise_info',
264             'exercise__description',
265             'exercise__course_module__course_instance__description',
266         )
267         # parent is prefetched because there may be multiple ancestors, and
268         # they are needed for building the deviation's URL.
269         .prefetch_related('exercise__parent')
270         .order_by('submitter', 'exercise__course_module')
271     )
272 
273     deviation_groups = groupby(
274         ordered_deviations,
275         lambda obj: (obj.submitter, obj.exercise.course_module),
276     )
277     for (_submitter, module), deviations_iter in deviation_groups:
278         deviations = list(deviations_iter)
279         can_group = True
280         show_granter = True
281         if len(deviations) < 2:
282             # Group must have at least 2 deviations.
283             can_group = False
284         else:
285             group_exercises = set()
286             # Check that the same deviation has been granted for all exercises.
287             first_granter = deviations[0].granter.id
288             for deviation in deviations:
289                 if not deviation.is_groupable(deviations[0]):
290                     can_group = False
291                     if not show_granter:
292                         break
293                 if deviation.granter.id != first_granter:
294                     show_granter = False
295                     if not can_group:
296                         break
297                 group_exercises.add(deviation.exercise.id)
298             else:
299                 if len(group_exercises) != exercise_count_by_module[module.id]:
300                     # The number of exercises that have deviations doesn't
301                     # match the number of exercises in the module, so there
302                     # are some exercises that don't have a deviation.
303                     can_group = False
304         group_id = f"{deviations[0].submitter.id}.{module.id}" if can_group else None
305         yield (deviations, can_group, group_id, show_granter)
306 
307 
308 def get_exercises(form_data: Dict[str, Any]) -> models.QuerySet[BaseExercise]:
309     """
310     Get the exercises that match the input form's `exercise` and `module`
311     fields.
312     """
313     return BaseExercise.objects.filter(
314         models.Q(id__in=form_data.get('exercise', []))
315         | models.Q(course_module__in=form_data.get('module', []))
316     )
317 
318 
319 def get_submitters(form_data: Dict[str, Any]) -> models.QuerySet[UserProfile]:
320     """
321     Get the submitters that match the input form's `submitter` and
322     `submitter_tag` fields.
323     """
324     return UserProfile.objects.filter(
325         models.Q(id__in=form_data.get('submitter', []))
326         | models.Q(taggings__tag__in=form_data.get('submitter_tag', []))
327     ).distinct()
328
```
--- 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/deviations/viewbase.py b/deviations/viewbase.py
--- a/deviations/viewbase.py
+++ b/deviations/viewbase.py
@@ -36,6 +36,14 @@
     deviation_model: Type[SubmissionRuleDeviation]
     session_key: str
 
+    def get_context_data(self, **kwargs: Any) -> dict:
+        context = super().get_context_data(**kwargs)
+        if self.request.GET.get('previous'):
+            context.update({'cancel_action': self.request.GET.get('previous')})
+        else:
+            context.update({'cancel_action': self.instance.get_url('deviations-list-dl')})
+        return context
+
     def get_form_kwargs(self) -> Dict[str, Any]:
         kwargs = super().get_form_kwargs()
         kwargs["instance"] = self.instance

{"golden_diff": "diff --git a/deviations/viewbase.py b/deviations/viewbase.py\n--- a/deviations/viewbase.py\n+++ b/deviations/viewbase.py\n@@ -36,6 +36,14 @@\n     deviation_model: Type[SubmissionRuleDeviation]\n     session_key: str\n \n+    def get_context_data(self, **kwargs: Any) -> dict:\n+        context = super().get_context_data(**kwargs)\n+        if self.request.GET.get('previous'):\n+            context.update({'cancel_action': self.request.GET.get('previous')})\n+        else:\n+            context.update({'cancel_action': self.instance.get_url('deviations-list-dl')})\n+        return context\n+\n     def get_form_kwargs(self) -> Dict[str, Any]:\n         kwargs = super().get_form_kwargs()\n         kwargs[\"instance\"] = self.instance\n", "issue": "The Cancel button in Add Deadline Deviations behaves surprisingly\nJust a detail:\r\n\r\nIf I go to a student\u2019s page, there are now links ther to add deadline deviations for that particular student. (This is super-useful.) \r\n\r\nNow say I click one of those buttons but then wish to cancel. There\u2019s the cancel button there on the add deviations page, but that doesn\u2019t actually cancel my action in the sense of taking me back where I was. It instead sends me to the list of all deviations (which page, in the case of the O1 course, either crashes with a Bad Gateway or takes approximately forever to load). \n", "before_files": [{"content": "from itertools import groupby\nfrom typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type\n\nfrom django.db import models\nfrom django.http import HttpRequest, HttpResponse\nfrom django.contrib import messages\nfrom django import forms\nfrom django.utils.text import format_lazy\nfrom django.utils.translation import gettext_lazy as _, ngettext\n\nfrom course.models import CourseModule, UserTag\nfrom course.viewbase import CourseInstanceMixin, CourseInstanceBaseView\nfrom deviations.models import SubmissionRuleDeviation\nfrom lib.helpers import is_ajax\nfrom lib.viewbase import BaseFormView, BaseRedirectView\nfrom authorization.permissions import ACCESS\nfrom exercise.models import BaseExercise\nfrom userprofile.models import UserProfile\n\n\nclass ListDeviationsView(CourseInstanceBaseView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n\n    def get_common_objects(self) -> None:\n        super().get_common_objects()\n        all_deviations = self.deviation_model.objects.filter(\n            exercise__course_module__course_instance=self.instance\n        )\n        self.deviation_groups = get_deviation_groups(all_deviations)\n        self.note(\"deviation_groups\")\n\n\nclass AddDeviationsView(CourseInstanceMixin, BaseFormView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n    session_key: str\n\n    def get_form_kwargs(self) -> Dict[str, Any]:\n        kwargs = super().get_form_kwargs()\n        kwargs[\"instance\"] = self.instance\n        return kwargs\n\n    def get_initial_get_param_spec(self) -> Dict[str, Optional[Callable[[str], Any]]]:\n        def list_arg(arg):\n            return arg.split(\",\")\n\n        spec = super().get_initial_get_param_spec()\n        spec.update({\n            \"module\": list_arg,\n            \"exercise\": list_arg,\n            \"submitter\": list_arg,\n            \"submitter_tag\": list_arg,\n        })\n        return spec\n\n    def form_valid(self, form: forms.BaseForm) -> HttpResponse:\n        exercises = get_exercises(form.cleaned_data)\n        submitters = get_submitters(form.cleaned_data)\n        existing_deviations = self.deviation_model.objects.filter(\n            exercise__in=exercises,\n            submitter__in=submitters,\n        )\n\n        if existing_deviations:\n            # Some deviations already existed. Use OverrideDeviationsView to\n            # confirm which ones the user wants to override. Store the form\n            # values in the current session, so they can be used afterwards.\n            self.success_url = self.deviation_model.get_override_url(self.instance)\n            self.request.session[self.session_key] = self.serialize_session_data(form.cleaned_data)\n        else:\n            self.success_url = self.deviation_model.get_list_url(self.instance)\n            for exercise in exercises:\n                for submitter in submitters:\n                    new_deviation = self.deviation_model(\n                        exercise=exercise,\n                        submitter=submitter,\n                        granter=self.request.user.userprofile,\n                    )\n                    new_deviation.update_by_form(form.cleaned_data)\n                    new_deviation.save()\n\n        return super().form_valid(form)\n\n    def serialize_session_data(self, form_data: Dict[str, Any]) -> Dict[str, Any]:\n        \"\"\"\n        Convert input form data into serializable values that can be stored in\n        the session cache.\n        \"\"\"\n        result = {}\n        for key in ('exercise', 'module', 'submitter', 'submitter_tag'):\n            result[key] = [i.id for i in form_data.get(key, [])]\n        return result\n\n\nclass OverrideDeviationsView(CourseInstanceMixin, BaseFormView):\n    access_mode = ACCESS.TEACHER\n    # form_class is not really used, but it is required by the FormView.\n    # The form contains only checkboxes and the user input is validated in\n    # the form_valid method. The form HTML is manually written in the template.\n    form_class = forms.Form\n    deviation_model: Type[SubmissionRuleDeviation]\n    session_key: str\n\n    def get_success_url(self) -> str:\n        return self.deviation_model.get_list_url(self.instance)\n\n    def get_common_objects(self) -> None:\n        super().get_common_objects()\n        self.session_data = self.deserialize_session_data(self.request.session[self.session_key])\n        self.exercises = get_exercises(self.session_data)\n        self.submitters = get_submitters(self.session_data)\n        self.existing_deviations = self.deviation_model.objects.filter(\n            exercise__in=self.exercises,\n            submitter__in=self.submitters,\n        )\n        self.deviation_groups = get_deviation_groups(self.existing_deviations)\n        self.note(\"session_data\", \"exercises\", \"submitters\", \"existing_deviations\", \"deviation_groups\")\n\n    def form_valid(self, form: forms.BaseForm) -> HttpResponse:\n        override_deviations = set()\n        deviation_list = self.request.POST.getlist('override')\n        for id_pair in deviation_list:\n            try:\n                submitter_id, exercise_id = id_pair.split('.')\n                submitter_id, exercise_id = int(submitter_id), int(exercise_id)\n                override_deviations.add((submitter_id, exercise_id))\n            except ValueError:\n                messages.error(self.request,\n                    format_lazy(\n                        _(\"INVALID_EXERCISE_OR_SUBMITTER_ID -- {id}\"),\n                        id=id_pair,\n                    )\n                )\n                continue\n\n        existing_deviations = {(d.submitter_id, d.exercise_id): d for d in self.existing_deviations}\n\n        for exercise in self.exercises:\n            for submitter in self.submitters:\n                existing_deviation = existing_deviations.get((submitter.id, exercise.id))\n                if existing_deviation is not None:\n                    if (submitter.id, exercise.id) in override_deviations:\n                        existing_deviation.granter = self.request.user.userprofile\n                        existing_deviation.update_by_form(self.session_data)\n                        existing_deviation.save()\n                else:\n                    new_deviation = self.deviation_model(\n                        exercise=exercise,\n                        submitter=submitter,\n                        granter=self.request.user.userprofile,\n                    )\n                    new_deviation.update_by_form(self.session_data)\n                    new_deviation.save()\n\n        del self.request.session[self.session_key]\n        return super().form_valid(form)\n\n    def deserialize_session_data(self, session_data: Dict[str, Any]) -> Dict[str, Any]:\n        \"\"\"\n        Convert serialized session data back into its original representation.\n        \"\"\"\n        result = {\n            'exercise': BaseExercise.objects.filter(id__in=session_data.get('exercise', [])),\n            'module': CourseModule.objects.filter(id__in=session_data.get('module', [])),\n            'submitter': UserProfile.objects.filter(id__in=session_data.get('submitter', [])),\n            'submitter_tag': UserTag.objects.filter(id__in=session_data.get('submitter_tag', [])),\n        }\n        return result\n\n\nclass RemoveDeviationsByIDView(CourseInstanceMixin, BaseRedirectView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n\n    def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:\n        deviations = self.deviation_model.objects.filter(\n            id__in=request.POST.getlist(\"id\"),\n            exercise__course_module__course_instance=self.instance,\n        )\n        for deviation in deviations:\n            deviation.delete()\n        if is_ajax(request):\n            return HttpResponse(status=204)\n        return self.redirect(self.deviation_model.get_list_url(self.instance))\n\n\nclass RemoveDeviationsView(CourseInstanceMixin, BaseFormView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n\n    def get_form_kwargs(self) -> Dict[str, Any]:\n        kwargs = super().get_form_kwargs()\n        kwargs[\"instance\"] = self.instance\n        return kwargs\n\n    def get_success_url(self) -> str:\n        return self.deviation_model.get_list_url(self.instance)\n\n    def form_valid(self, form: forms.BaseForm) -> HttpResponse:\n        number_of_removed = 0\n        deviations = self.deviation_model.objects.filter(\n            exercise__in=get_exercises(form.cleaned_data),\n            submitter__in=get_submitters(form.cleaned_data),\n        )\n        for deviation in deviations:\n            deviation.delete()\n            number_of_removed += 1\n        if number_of_removed == 0:\n            messages.warning(self.request, _(\"NOTHING_REMOVED\"))\n        else:\n            message = ngettext(\n                'REMOVED_DEVIATION -- {count}',\n                'REMOVED_DEVIATIONS -- {count}',\n                number_of_removed,\n            ).format(count=number_of_removed)\n            messages.info(self.request, message)\n        return super().form_valid(form)\n\n\n# pylint: disable-next=too-many-locals\ndef get_deviation_groups(\n        all_deviations: models.QuerySet[SubmissionRuleDeviation],\n        ) -> Iterable[Tuple[List[SubmissionRuleDeviation], bool, Optional[str]]]:\n    \"\"\"\n    Group the deviations by user and module.\n\n    Grouping condition: deviations can be grouped if the user has been\n    granted the same deviation (based on the `is_equal` method) for all\n    exercises in the module.\n\n    The returned tuples contain the following values:\n    1. List of deviations with the same user and module.\n    2. Boolean representing whether the deviations in the list can be\n    displayed as a group (i.e. the grouping condition is satisfied).\n    3. An id that uniquely identifies the group of deviations.\n    \"\"\"\n    # Find the number of exercises in each module.\n    course_instances = (\n        all_deviations\n        .values_list('exercise__course_module__course_instance', flat=True)\n        .distinct()\n    )\n    exercise_counts = (\n        BaseExercise.objects.filter(\n            course_module__course_instance__in=course_instances\n        )\n        .order_by()\n        .values('course_module_id')\n        .annotate(count=models.Count('*'))\n    )\n    exercise_count_by_module = {row['course_module_id']: row['count'] for row in exercise_counts}\n\n    ordered_deviations = (\n        all_deviations\n        .select_related(\n            'submitter', 'submitter__user',\n            'granter', 'granter__user',\n            'exercise', 'exercise__course_module',\n            'exercise__course_module__course_instance',\n        )\n        .defer(\n            'exercise__exercise_info',\n            'exercise__description',\n            'exercise__course_module__course_instance__description',\n        )\n        # parent is prefetched because there may be multiple ancestors, and\n        # they are needed for building the deviation's URL.\n        .prefetch_related('exercise__parent')\n        .order_by('submitter', 'exercise__course_module')\n    )\n\n    deviation_groups = groupby(\n        ordered_deviations,\n        lambda obj: (obj.submitter, obj.exercise.course_module),\n    )\n    for (_submitter, module), deviations_iter in deviation_groups:\n        deviations = list(deviations_iter)\n        can_group = True\n        show_granter = True\n        if len(deviations) < 2:\n            # Group must have at least 2 deviations.\n            can_group = False\n        else:\n            group_exercises = set()\n            # Check that the same deviation has been granted for all exercises.\n            first_granter = deviations[0].granter.id\n            for deviation in deviations:\n                if not deviation.is_groupable(deviations[0]):\n                    can_group = False\n                    if not show_granter:\n                        break\n                if deviation.granter.id != first_granter:\n                    show_granter = False\n                    if not can_group:\n                        break\n                group_exercises.add(deviation.exercise.id)\n            else:\n                if len(group_exercises) != exercise_count_by_module[module.id]:\n                    # The number of exercises that have deviations doesn't\n                    # match the number of exercises in the module, so there\n                    # are some exercises that don't have a deviation.\n                    can_group = False\n        group_id = f\"{deviations[0].submitter.id}.{module.id}\" if can_group else None\n        yield (deviations, can_group, group_id, show_granter)\n\n\ndef get_exercises(form_data: Dict[str, Any]) -> models.QuerySet[BaseExercise]:\n    \"\"\"\n    Get the exercises that match the input form's `exercise` and `module`\n    fields.\n    \"\"\"\n    return BaseExercise.objects.filter(\n        models.Q(id__in=form_data.get('exercise', []))\n        | models.Q(course_module__in=form_data.get('module', []))\n    )\n\n\ndef get_submitters(form_data: Dict[str, Any]) -> models.QuerySet[UserProfile]:\n    \"\"\"\n    Get the submitters that match the input form's `submitter` and\n    `submitter_tag` fields.\n    \"\"\"\n    return UserProfile.objects.filter(\n        models.Q(id__in=form_data.get('submitter', []))\n        | models.Q(taggings__tag__in=form_data.get('submitter_tag', []))\n    ).distinct()\n", "path": "deviations/viewbase.py"}], "after_files": [{"content": "from itertools import groupby\nfrom typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type\n\nfrom django.db import models\nfrom django.http import HttpRequest, HttpResponse\nfrom django.contrib import messages\nfrom django import forms\nfrom django.utils.text import format_lazy\nfrom django.utils.translation import gettext_lazy as _, ngettext\n\nfrom course.models import CourseModule, UserTag\nfrom course.viewbase import CourseInstanceMixin, CourseInstanceBaseView\nfrom deviations.models import SubmissionRuleDeviation\nfrom lib.helpers import is_ajax\nfrom lib.viewbase import BaseFormView, BaseRedirectView\nfrom authorization.permissions import ACCESS\nfrom exercise.models import BaseExercise\nfrom userprofile.models import UserProfile\n\n\nclass ListDeviationsView(CourseInstanceBaseView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n\n    def get_common_objects(self) -> None:\n        super().get_common_objects()\n        all_deviations = self.deviation_model.objects.filter(\n            exercise__course_module__course_instance=self.instance\n        )\n        self.deviation_groups = get_deviation_groups(all_deviations)\n        self.note(\"deviation_groups\")\n\n\nclass AddDeviationsView(CourseInstanceMixin, BaseFormView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n    session_key: str\n\n    def get_context_data(self, **kwargs: Any) -> dict:\n        context = super().get_context_data(**kwargs)\n        if self.request.GET.get('previous'):\n            context.update({'cancel_action': self.request.GET.get('previous')})\n        else:\n            context.update({'cancel_action': self.instance.get_url('deviations-list-dl')})\n        return context\n\n    def get_form_kwargs(self) -> Dict[str, Any]:\n        kwargs = super().get_form_kwargs()\n        kwargs[\"instance\"] = self.instance\n        return kwargs\n\n    def get_initial_get_param_spec(self) -> Dict[str, Optional[Callable[[str], Any]]]:\n        def list_arg(arg):\n            return arg.split(\",\")\n\n        spec = super().get_initial_get_param_spec()\n        spec.update({\n            \"module\": list_arg,\n            \"exercise\": list_arg,\n            \"submitter\": list_arg,\n            \"submitter_tag\": list_arg,\n        })\n        return spec\n\n    def form_valid(self, form: forms.BaseForm) -> HttpResponse:\n        exercises = get_exercises(form.cleaned_data)\n        submitters = get_submitters(form.cleaned_data)\n        existing_deviations = self.deviation_model.objects.filter(\n            exercise__in=exercises,\n            submitter__in=submitters,\n        )\n\n        if existing_deviations:\n            # Some deviations already existed. Use OverrideDeviationsView to\n            # confirm which ones the user wants to override. Store the form\n            # values in the current session, so they can be used afterwards.\n            self.success_url = self.deviation_model.get_override_url(self.instance)\n            self.request.session[self.session_key] = self.serialize_session_data(form.cleaned_data)\n        else:\n            self.success_url = self.deviation_model.get_list_url(self.instance)\n            for exercise in exercises:\n                for submitter in submitters:\n                    new_deviation = self.deviation_model(\n                        exercise=exercise,\n                        submitter=submitter,\n                        granter=self.request.user.userprofile,\n                    )\n                    new_deviation.update_by_form(form.cleaned_data)\n                    new_deviation.save()\n\n        return super().form_valid(form)\n\n    def serialize_session_data(self, form_data: Dict[str, Any]) -> Dict[str, Any]:\n        \"\"\"\n        Convert input form data into serializable values that can be stored in\n        the session cache.\n        \"\"\"\n        result = {}\n        for key in ('exercise', 'module', 'submitter', 'submitter_tag'):\n            result[key] = [i.id for i in form_data.get(key, [])]\n        return result\n\n\nclass OverrideDeviationsView(CourseInstanceMixin, BaseFormView):\n    access_mode = ACCESS.TEACHER\n    # form_class is not really used, but it is required by the FormView.\n    # The form contains only checkboxes and the user input is validated in\n    # the form_valid method. The form HTML is manually written in the template.\n    form_class = forms.Form\n    deviation_model: Type[SubmissionRuleDeviation]\n    session_key: str\n\n    def get_success_url(self) -> str:\n        return self.deviation_model.get_list_url(self.instance)\n\n    def get_common_objects(self) -> None:\n        super().get_common_objects()\n        self.session_data = self.deserialize_session_data(self.request.session[self.session_key])\n        self.exercises = get_exercises(self.session_data)\n        self.submitters = get_submitters(self.session_data)\n        self.existing_deviations = self.deviation_model.objects.filter(\n            exercise__in=self.exercises,\n            submitter__in=self.submitters,\n        )\n        self.deviation_groups = get_deviation_groups(self.existing_deviations)\n        self.note(\"session_data\", \"exercises\", \"submitters\", \"existing_deviations\", \"deviation_groups\")\n\n    def form_valid(self, form: forms.BaseForm) -> HttpResponse:\n        override_deviations = set()\n        deviation_list = self.request.POST.getlist('override')\n        for id_pair in deviation_list:\n            try:\n                submitter_id, exercise_id = id_pair.split('.')\n                submitter_id, exercise_id = int(submitter_id), int(exercise_id)\n                override_deviations.add((submitter_id, exercise_id))\n            except ValueError:\n                messages.error(self.request,\n                    format_lazy(\n                        _(\"INVALID_EXERCISE_OR_SUBMITTER_ID -- {id}\"),\n                        id=id_pair,\n                    )\n                )\n                continue\n\n        existing_deviations = {(d.submitter_id, d.exercise_id): d for d in self.existing_deviations}\n\n        for exercise in self.exercises:\n            for submitter in self.submitters:\n                existing_deviation = existing_deviations.get((submitter.id, exercise.id))\n                if existing_deviation is not None:\n                    if (submitter.id, exercise.id) in override_deviations:\n                        existing_deviation.granter = self.request.user.userprofile\n                        existing_deviation.update_by_form(self.session_data)\n                        existing_deviation.save()\n                else:\n                    new_deviation = self.deviation_model(\n                        exercise=exercise,\n                        submitter=submitter,\n                        granter=self.request.user.userprofile,\n                    )\n                    new_deviation.update_by_form(self.session_data)\n                    new_deviation.save()\n\n        del self.request.session[self.session_key]\n        return super().form_valid(form)\n\n    def deserialize_session_data(self, session_data: Dict[str, Any]) -> Dict[str, Any]:\n        \"\"\"\n        Convert serialized session data back into its original representation.\n        \"\"\"\n        result = {\n            'exercise': BaseExercise.objects.filter(id__in=session_data.get('exercise', [])),\n            'module': CourseModule.objects.filter(id__in=session_data.get('module', [])),\n            'submitter': UserProfile.objects.filter(id__in=session_data.get('submitter', [])),\n            'submitter_tag': UserTag.objects.filter(id__in=session_data.get('submitter_tag', [])),\n        }\n        return result\n\n\nclass RemoveDeviationsByIDView(CourseInstanceMixin, BaseRedirectView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n\n    def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:\n        deviations = self.deviation_model.objects.filter(\n            id__in=request.POST.getlist(\"id\"),\n            exercise__course_module__course_instance=self.instance,\n        )\n        for deviation in deviations:\n            deviation.delete()\n        if is_ajax(request):\n            return HttpResponse(status=204)\n        return self.redirect(self.deviation_model.get_list_url(self.instance))\n\n\nclass RemoveDeviationsView(CourseInstanceMixin, BaseFormView):\n    access_mode = ACCESS.TEACHER\n    deviation_model: Type[SubmissionRuleDeviation]\n\n    def get_form_kwargs(self) -> Dict[str, Any]:\n        kwargs = super().get_form_kwargs()\n        kwargs[\"instance\"] = self.instance\n        return kwargs\n\n    def get_success_url(self) -> str:\n        return self.deviation_model.get_list_url(self.instance)\n\n    def form_valid(self, form: forms.BaseForm) -> HttpResponse:\n        number_of_removed = 0\n        deviations = self.deviation_model.objects.filter(\n            exercise__in=get_exercises(form.cleaned_data),\n            submitter__in=get_submitters(form.cleaned_data),\n        )\n        for deviation in deviations:\n            deviation.delete()\n            number_of_removed += 1\n        if number_of_removed == 0:\n            messages.warning(self.request, _(\"NOTHING_REMOVED\"))\n        else:\n            message = ngettext(\n                'REMOVED_DEVIATION -- {count}',\n                'REMOVED_DEVIATIONS -- {count}',\n                number_of_removed,\n            ).format(count=number_of_removed)\n            messages.info(self.request, message)\n        return super().form_valid(form)\n\n\n# pylint: disable-next=too-many-locals\ndef get_deviation_groups(\n        all_deviations: models.QuerySet[SubmissionRuleDeviation],\n        ) -> Iterable[Tuple[List[SubmissionRuleDeviation], bool, Optional[str]]]:\n    \"\"\"\n    Group the deviations by user and module.\n\n    Grouping condition: deviations can be grouped if the user has been\n    granted the same deviation (based on the `is_equal` method) for all\n    exercises in the module.\n\n    The returned tuples contain the following values:\n    1. List of deviations with the same user and module.\n    2. Boolean representing whether the deviations in the list can be\n    displayed as a group (i.e. the grouping condition is satisfied).\n    3. An id that uniquely identifies the group of deviations.\n    \"\"\"\n    # Find the number of exercises in each module.\n    course_instances = (\n        all_deviations\n        .values_list('exercise__course_module__course_instance', flat=True)\n        .distinct()\n    )\n    exercise_counts = (\n        BaseExercise.objects.filter(\n            course_module__course_instance__in=course_instances\n        )\n        .order_by()\n        .values('course_module_id')\n        .annotate(count=models.Count('*'))\n    )\n    exercise_count_by_module = {row['course_module_id']: row['count'] for row in exercise_counts}\n\n    ordered_deviations = (\n        all_deviations\n        .select_related(\n            'submitter', 'submitter__user',\n            'granter', 'granter__user',\n            'exercise', 'exercise__course_module',\n        )\n        # parent is prefetched because there may be multiple ancestors, and\n        # they are needed for building the deviation's URL.\n        .prefetch_related('exercise__parent')\n        .order_by('submitter', 'exercise__course_module')\n    )\n\n    deviation_groups = groupby(\n        ordered_deviations,\n        lambda obj: (obj.submitter, obj.exercise.course_module),\n    )\n    for (_submitter, module), deviations_iter in deviation_groups:\n        deviations = list(deviations_iter)\n        can_group = True\n        show_granter = True\n        if len(deviations) < 2:\n            # Group must have at least 2 deviations.\n            can_group = False\n        else:\n            group_exercises = set()\n            # Check that the same deviation has been granted for all exercises.\n            first_granter = deviations[0].granter.id\n            for deviation in deviations:\n                if not deviation.is_groupable(deviations[0]):\n                    can_group = False\n                    if not show_granter:\n                        break\n                if deviation.granter.id != first_granter:\n                    show_granter = False\n                    if not can_group:\n                        break\n                group_exercises.add(deviation.exercise.id)\n            else:\n                if len(group_exercises) != exercise_count_by_module[module.id]:\n                    # The number of exercises that have deviations doesn't\n                    # match the number of exercises in the module, so there\n                    # are some exercises that don't have a deviation.\n                    can_group = False\n        group_id = f\"{deviations[0].submitter.id}.{module.id}\" if can_group else None\n        yield (deviations, can_group, group_id, show_granter)\n\n\ndef get_exercises(form_data: Dict[str, Any]) -> models.QuerySet[BaseExercise]:\n    \"\"\"\n    Get the exercises that match the input form's `exercise` and `module`\n    fields.\n    \"\"\"\n    return BaseExercise.objects.filter(\n        models.Q(id__in=form_data.get('exercise', []))\n        | models.Q(course_module__in=form_data.get('module', []))\n    )\n\n\ndef get_submitters(form_data: Dict[str, Any]) -> models.QuerySet[UserProfile]:\n    \"\"\"\n    Get the submitters that match the input form's `submitter` and\n    `submitter_tag` fields.\n    \"\"\"\n    return UserProfile.objects.filter(\n        models.Q(id__in=form_data.get('submitter', []))\n        | models.Q(taggings__tag__in=form_data.get('submitter_tag', []))\n    ).distinct()\n", "path": "deviations/viewbase.py"}]}