We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Custom talk deletion logic breaks with Django >= 4.0
The custom logic to turn talk deletions into withdrawals doesn't work with Django >= 4.0

Quoting from the Django 4 release notes

```In accordance with FormMixin, object deletion for POST requests is handled in form_valid(). Custom delete logic in delete() handlers should be moved to form_valid(), or a shared helper method, as needed.```

The change is mechanically fairly simple, but I still need to look how this works the older Dango releases we support.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wafer/talks/views.py`
Content:
```
1 from itertools import groupby
2 
3 from django.conf import settings
4 from django.contrib.auth.mixins import (
5     LoginRequiredMixin, PermissionRequiredMixin)
6 from django.contrib.auth.models import AnonymousUser
7 from django.core.exceptions import PermissionDenied, ValidationError
8 from django.db.models import Q, F
9 from django.http import Http404
10 from django.http import HttpResponseRedirect
11 from django.urls import reverse_lazy
12 from django.views.generic.edit import CreateView, UpdateView, DeleteView
13 
14 from bakery.views import BuildableDetailView, BuildableListView
15 from rest_framework import viewsets
16 from rest_framework.permissions import (
17     DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly,
18     BasePermission)
19 from rest_framework_extensions.mixins import NestedViewSetMixin
20 from reversion import revisions
21 from reversion.models import Version
22 
23 from wafer.talks.models import (
24     Review, Talk, TalkType, TalkUrl, Track,
25     ACCEPTED, CANCELLED, SUBMITTED, UNDER_CONSIDERATION, WITHDRAWN)
26 from wafer.talks.forms import ReviewForm, get_talk_form_class
27 from wafer.talks.serializers import TalkSerializer, TalkUrlSerializer
28 from wafer.users.models import UserProfile
29 from wafer.utils import order_results_by, PaginatedBuildableListView
30 
31 
32 class EditOwnTalksMixin(object):
33     '''Users can edit their own talks as long as the talk is
34        "Under Consideration"'''
35     def get_object(self, *args, **kwargs):
36         object_ = super().get_object(*args, **kwargs)
37         if object_.can_edit(self.request.user):
38             return object_
39         else:
40             raise PermissionDenied
41 
42 
43 class UsersTalks(PaginatedBuildableListView):
44     template_name = 'wafer.talks/talks.html'
45     build_prefix = 'talks'
46     paginate_by = 100
47 
48     @order_results_by('talk_type', 'talk_id')
49     def get_queryset(self):
50         # self.request will be None when we come here via the static site
51         # renderer
52         if self.request and Talk.can_view_all(self.request.user):
53             talks = Talk.objects.all()
54         else:
55             talks = Talk.objects.filter(Q(status=ACCEPTED) | Q(status=CANCELLED))
56         return talks.prefetch_related(
57             "talk_type", "corresponding_author", "authors", "authors__userprofile"
58         )
59 
60     def get_context_data(self, **kwargs):
61         context = super().get_context_data(**kwargs)
62         context["languages"] = Talk.LANGUAGES
63         context["see_all"] = Talk.can_view_all(self.request.user)
64         return context
65 
66 
67 class TalkView(BuildableDetailView):
68     template_name = 'wafer.talks/talk.html'
69     model = Talk
70 
71     # Needed so django-bakery only renders public talks
72     def build_object(self, obj):
73         """Override django-bakery to skip talks that raise 403"""
74         try:
75             super().build_object(obj)
76         except PermissionDenied:
77             # We cleanup the directory created
78             self.unbuild_object(obj)
79 
80     def create_request(self, path):
81         request = super().create_request(path)
82         request.user = AnonymousUser()
83         return request
84 
85     def get_object(self, *args, **kwargs):
86         '''Only talk owners can see talks, unless they've been accepted'''
87         object_ = super().get_object(*args, **kwargs)
88         if not object_.can_view(self.request.user):
89             raise PermissionDenied
90         return object_
91 
92     def canonical_url(self):
93         '''Return the canonical URL for this view'''
94         return self.object.get_absolute_url()
95 
96     def render_to_response(self, *args, **kwargs):
97         '''Canonicalize the URL if the slug changed'''
98         canonical_url = self.canonical_url()
99         if self.request.path != canonical_url:
100             return HttpResponseRedirect(canonical_url)
101         return super().render_to_response(*args, **kwargs)
102 
103     def get_context_data(self, **kwargs):
104         context = super().get_context_data(**kwargs)
105         talk = self.object
106         user = self.request.user
107 
108         context['can_edit'] = talk.can_edit(user)
109 
110         can_review = talk.can_review(user)
111         context['can_review'] = can_review
112         if can_review:
113             review = talk.reviews.filter(reviewer=user).first()
114             context['review_form'] = ReviewForm(
115                 instance=review, talk=talk, user=user)
116 
117         return context
118 
119 
120 class TalkCreate(LoginRequiredMixin, CreateView):
121     model = Talk
122     template_name = 'wafer.talks/talk_form.html'
123 
124     def get_form_class(self):
125         return get_talk_form_class()
126 
127     def get_form_kwargs(self):
128         kwargs = super().get_form_kwargs()
129         kwargs['user'] = self.request.user
130         return kwargs
131 
132     def get_context_data(self, **kwargs):
133         context = super().get_context_data(**kwargs)
134         can_submit = getattr(settings, 'WAFER_TALKS_OPEN', True)
135         if can_submit and TalkType.objects.exists():
136             # Check for all talk types being disabled
137             can_submit = TalkType.objects.open_for_submission().count() > 0
138         context['can_submit'] = can_submit
139         return context
140 
141     @revisions.create_revision()
142     def form_valid(self, form):
143         if not getattr(settings, 'WAFER_TALKS_OPEN', True):
144             # Should this be SuspiciousOperation?
145             raise ValidationError("Talk submission isn't open")
146         # Eaaargh we have to do the work of CreateView if we want to set values
147         # before saving
148         self.object = form.save(commit=False)
149         self.object.corresponding_author = self.request.user
150         self.object.save()
151         revisions.set_user(self.request.user)
152         revisions.set_comment("Talk Created")
153         # Save the author information as well (many-to-many fun)
154         form.save_m2m()
155         return HttpResponseRedirect(self.get_success_url())
156 
157 
158 class TalkUpdate(EditOwnTalksMixin, UpdateView):
159     model = Talk
160     template_name = 'wafer.talks/talk_form.html'
161 
162     def get_form_class(self):
163         return get_talk_form_class()
164 
165     def get_form_kwargs(self):
166         kwargs = super().get_form_kwargs()
167         kwargs['user'] = self.request.user
168         return kwargs
169 
170     def get_context_data(self, **kwargs):
171         context = super().get_context_data(**kwargs)
172         context['can_edit'] = self.object.can_edit(self.request.user)
173         return context
174 
175     @revisions.create_revision()
176     def form_valid(self, form):
177         revisions.set_user(self.request.user)
178         revisions.set_comment("Talk Modified")
179         return super().form_valid(form)
180 
181 
182 class TalkWithdraw(EditOwnTalksMixin, DeleteView):
183     model = Talk
184     template_name = 'wafer.talks/talk_withdraw.html'
185     success_url = reverse_lazy('wafer_page')
186 
187     @revisions.create_revision()
188     def delete(self, request, *args, **kwargs):
189         """Override delete to only withdraw"""
190         talk = self.get_object()
191         talk.status = WITHDRAWN
192         talk.save()
193         revisions.set_user(self.request.user)
194         revisions.set_comment("Talk Withdrawn")
195         return HttpResponseRedirect(self.success_url)
196 
197 
198 class TalkReview(PermissionRequiredMixin, CreateView):
199     model = Review
200     form_class = ReviewForm
201     permission_required = 'talks.add_review'
202     template_name = 'wafer.talks/review_talk.html'
203 
204     def get_form_kwargs(self):
205         kwargs = super().get_form_kwargs()
206         kwargs['talk'] = Talk.objects.get(pk=self.kwargs['pk'])
207         kwargs['instance'] = self.get_object()
208         kwargs['user'] = self.request.user
209         return kwargs
210 
211     def get_object(self):
212         try:
213             return Review.objects.get(
214                 talk_id=self.kwargs['pk'], reviewer=self.request.user)
215         except Review.DoesNotExist:
216             return None
217 
218     def form_valid(self, form):
219         existing = self.get_object()
220         response = super().form_valid(form)
221 
222         review = self.get_object()
223         # Update the talk to 'under consideration' if a review is
224         # added.
225         talk = review.talk
226         if talk.status == SUBMITTED:
227             talk.status = UNDER_CONSIDERATION
228             with revisions.create_revision():
229                 revisions.set_user(self.request.user)
230                 revisions.set_comment("Status changed by review process")
231                 talk.save()
232 
233         # Create the revision
234         # Note that we do this after the review has been saved
235         # (without a revision) in the super().form_valid call and
236         # after the scores have been added, so that the object_repr
237         # is correct
238         # We also do this after potentially updating the talk, so
239         # that the review revision time is correct for is_current
240         with revisions.create_revision():
241             revisions.set_user(self.request.user)
242             if existing:
243                 revisions.set_comment("Review Modified")
244             else:
245                 revisions.set_comment("Review Created")
246             review.save()
247 
248         return response
249 
250     def get_success_url(self):
251         return self.get_object().talk.get_absolute_url()
252 
253 
254 class Speakers(BuildableListView):
255     model = Talk
256     template_name = 'wafer.talks/speakers.html'
257     build_path = 'talks/speakers/index.html'
258 
259     def _by_row(self, speakers, n):
260         return [speakers[i:i + n] for i in range(0, len(speakers), n)]
261 
262     def get_context_data(self, **kwargs):
263         context = super().get_context_data(**kwargs)
264         speakers = UserProfile.objects.filter(
265             user__talks__status='A').distinct().prefetch_related(
266             'user').order_by('user__talks__talk_type',
267                              'user__first_name',
268                              'user__last_name',
269                              'user__username').annotate(
270             talk_type=F('user__talks__talk_type__name'),
271             show_speakers=F('user__talks__talk_type__show_speakers'))
272         bytype = groupby(speakers, lambda x: x.talk_type)
273         context['speaker_rows'] = {}
274         for talk_type, type_speakers in bytype:
275             type_speakers = list(type_speakers)
276             # We explicitly check for False, as no talk type will give us None for
277             # show_speakers and we want to default to including that
278             if type_speakers and type_speakers[0].show_speakers is not False:
279                 context["speaker_rows"][talk_type] = self._by_row(type_speakers, 4)
280         return context
281 
282 
283 class TracksView(BuildableListView):
284     model = Track
285     template_name = 'wafer.talks/talk_tracks.html'
286     build_path = 'talks/tracks/index.html'
287 
288 
289 class TalkTypesView(BuildableListView):
290     model = TalkType
291     template_name = 'wafer.talks/talk_types.html'
292     build_path = 'talks/types/index.html'
293 
294 
295 class TalksViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
296     """API endpoint that allows talks to be viewed or edited."""
297     queryset = Talk.objects.none()  # Needed for the REST Permissions
298     serializer_class = TalkSerializer
299     # XXX: Do we want to allow authors to edit talks via the API?
300     permission_classes = (DjangoModelPermissionsOrAnonReadOnly, )
301 
302     @order_results_by('talk_type', 'talk_id')
303     def get_queryset(self):
304         # We override the default implementation to only show accepted talks
305         # to people who aren't part of the management group
306         if self.request.user.id is None:
307             # Anonymous user, so just accepted or cancelled talks
308             return Talk.objects.filter(Q(status=ACCEPTED) |
309                                        Q(status=CANCELLED))
310         elif Talk.can_view_all(self.request.user):
311             return Talk.objects.all()
312         else:
313             # Also include talks owned by the user
314             # XXX: Should this be all authors rather than just
315             # the corresponding author?
316             return Talk.objects.filter(
317                 Q(status=ACCEPTED) |
318                 Q(status=CANCELLED) |
319                 Q(corresponding_author=self.request.user))
320 
321 
322 class TalkExistsPermission(BasePermission):
323     def has_permission(self, request, view):
324         talk_id = view.get_parents_query_dict()['talk']
325         if not Talk.objects.filter(pk=talk_id).exists():
326             raise Http404
327         return True
328 
329 
330 class TalkUrlsViewSet(NestedViewSetMixin, viewsets.ModelViewSet):
331     """API endpoint that allows talks to be viewed or edited."""
332     queryset = TalkUrl.objects.all().order_by('id')
333     serializer_class = TalkUrlSerializer
334     permission_classes = (DjangoModelPermissions, TalkExistsPermission)
335 
336     def create(self, request, *args, **kw):
337         request.data['talk'] = self.get_parents_query_dict()['talk']
338         return super().create(request, *args, **kw)
339 
340     def update(self, request, *args, **kw):
341         request.data['talk'] = self.get_parents_query_dict()['talk']
342         return super().update(request, *args, **kw)
343
```
--- 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/wafer/talks/views.py b/wafer/talks/views.py
--- a/wafer/talks/views.py
+++ b/wafer/talks/views.py
@@ -184,9 +184,10 @@
     template_name = 'wafer.talks/talk_withdraw.html'
     success_url = reverse_lazy('wafer_page')
 
+
     @revisions.create_revision()
-    def delete(self, request, *args, **kwargs):
-        """Override delete to only withdraw"""
+    def withdraw_helper(self, request):
+        """Handle the logic for withdrawing a talk"""
         talk = self.get_object()
         talk.status = WITHDRAWN
         talk.save()
@@ -194,6 +195,14 @@
         revisions.set_comment("Talk Withdrawn")
         return HttpResponseRedirect(self.success_url)
 
+    def delete(self, request, *args, **kwargs):
+        """Override delete to only withdraw for Django < 4"""
+        return self.withdraw_helper(request)
+
+    def form_valid(self, request, *args, **kwargs):
+        """Override delete to only withdraw for Django >= 4.0"""
+        return self.withdraw_helper(request)
+
 
 class TalkReview(PermissionRequiredMixin, CreateView):
     model = Review

{"golden_diff": "diff --git a/wafer/talks/views.py b/wafer/talks/views.py\n--- a/wafer/talks/views.py\n+++ b/wafer/talks/views.py\n@@ -184,9 +184,10 @@\n     template_name = 'wafer.talks/talk_withdraw.html'\n     success_url = reverse_lazy('wafer_page')\n \n+\n     @revisions.create_revision()\n-    def delete(self, request, *args, **kwargs):\n-        \"\"\"Override delete to only withdraw\"\"\"\n+    def withdraw_helper(self, request):\n+        \"\"\"Handle the logic for withdrawing a talk\"\"\"\n         talk = self.get_object()\n         talk.status = WITHDRAWN\n         talk.save()\n@@ -194,6 +195,14 @@\n         revisions.set_comment(\"Talk Withdrawn\")\n         return HttpResponseRedirect(self.success_url)\n \n+    def delete(self, request, *args, **kwargs):\n+        \"\"\"Override delete to only withdraw for Django < 4\"\"\"\n+        return self.withdraw_helper(request)\n+\n+    def form_valid(self, request, *args, **kwargs):\n+        \"\"\"Override delete to only withdraw for Django >= 4.0\"\"\"\n+        return self.withdraw_helper(request)\n+\n \n class TalkReview(PermissionRequiredMixin, CreateView):\n     model = Review\n", "issue": "Custom talk deletion logic breaks with Django >= 4.0\nThe custom logic to turn talk deletions into withdrawals doesn't work with Django >= 4.0\r\n\r\nQuoting from the Django 4 release notes\r\n\r\n```In accordance with FormMixin, object deletion for POST requests is handled in form_valid(). Custom delete logic in delete() handlers should be moved to form_valid(), or a shared helper method, as needed.```\r\n\r\nThe change is mechanically fairly simple, but I still need to look how this works the older Dango releases we support.\n", "before_files": [{"content": "from itertools import groupby\n\nfrom django.conf import settings\nfrom django.contrib.auth.mixins import (\n    LoginRequiredMixin, PermissionRequiredMixin)\nfrom django.contrib.auth.models import AnonymousUser\nfrom django.core.exceptions import PermissionDenied, ValidationError\nfrom django.db.models import Q, F\nfrom django.http import Http404\nfrom django.http import HttpResponseRedirect\nfrom django.urls import reverse_lazy\nfrom django.views.generic.edit import CreateView, UpdateView, DeleteView\n\nfrom bakery.views import BuildableDetailView, BuildableListView\nfrom rest_framework import viewsets\nfrom rest_framework.permissions import (\n    DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly,\n    BasePermission)\nfrom rest_framework_extensions.mixins import NestedViewSetMixin\nfrom reversion import revisions\nfrom reversion.models import Version\n\nfrom wafer.talks.models import (\n    Review, Talk, TalkType, TalkUrl, Track,\n    ACCEPTED, CANCELLED, SUBMITTED, UNDER_CONSIDERATION, WITHDRAWN)\nfrom wafer.talks.forms import ReviewForm, get_talk_form_class\nfrom wafer.talks.serializers import TalkSerializer, TalkUrlSerializer\nfrom wafer.users.models import UserProfile\nfrom wafer.utils import order_results_by, PaginatedBuildableListView\n\n\nclass EditOwnTalksMixin(object):\n    '''Users can edit their own talks as long as the talk is\n       \"Under Consideration\"'''\n    def get_object(self, *args, **kwargs):\n        object_ = super().get_object(*args, **kwargs)\n        if object_.can_edit(self.request.user):\n            return object_\n        else:\n            raise PermissionDenied\n\n\nclass UsersTalks(PaginatedBuildableListView):\n    template_name = 'wafer.talks/talks.html'\n    build_prefix = 'talks'\n    paginate_by = 100\n\n    @order_results_by('talk_type', 'talk_id')\n    def get_queryset(self):\n        # self.request will be None when we come here via the static site\n        # renderer\n        if self.request and Talk.can_view_all(self.request.user):\n            talks = Talk.objects.all()\n        else:\n            talks = Talk.objects.filter(Q(status=ACCEPTED) | Q(status=CANCELLED))\n        return talks.prefetch_related(\n            \"talk_type\", \"corresponding_author\", \"authors\", \"authors__userprofile\"\n        )\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context[\"languages\"] = Talk.LANGUAGES\n        context[\"see_all\"] = Talk.can_view_all(self.request.user)\n        return context\n\n\nclass TalkView(BuildableDetailView):\n    template_name = 'wafer.talks/talk.html'\n    model = Talk\n\n    # Needed so django-bakery only renders public talks\n    def build_object(self, obj):\n        \"\"\"Override django-bakery to skip talks that raise 403\"\"\"\n        try:\n            super().build_object(obj)\n        except PermissionDenied:\n            # We cleanup the directory created\n            self.unbuild_object(obj)\n\n    def create_request(self, path):\n        request = super().create_request(path)\n        request.user = AnonymousUser()\n        return request\n\n    def get_object(self, *args, **kwargs):\n        '''Only talk owners can see talks, unless they've been accepted'''\n        object_ = super().get_object(*args, **kwargs)\n        if not object_.can_view(self.request.user):\n            raise PermissionDenied\n        return object_\n\n    def canonical_url(self):\n        '''Return the canonical URL for this view'''\n        return self.object.get_absolute_url()\n\n    def render_to_response(self, *args, **kwargs):\n        '''Canonicalize the URL if the slug changed'''\n        canonical_url = self.canonical_url()\n        if self.request.path != canonical_url:\n            return HttpResponseRedirect(canonical_url)\n        return super().render_to_response(*args, **kwargs)\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        talk = self.object\n        user = self.request.user\n\n        context['can_edit'] = talk.can_edit(user)\n\n        can_review = talk.can_review(user)\n        context['can_review'] = can_review\n        if can_review:\n            review = talk.reviews.filter(reviewer=user).first()\n            context['review_form'] = ReviewForm(\n                instance=review, talk=talk, user=user)\n\n        return context\n\n\nclass TalkCreate(LoginRequiredMixin, CreateView):\n    model = Talk\n    template_name = 'wafer.talks/talk_form.html'\n\n    def get_form_class(self):\n        return get_talk_form_class()\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs['user'] = self.request.user\n        return kwargs\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        can_submit = getattr(settings, 'WAFER_TALKS_OPEN', True)\n        if can_submit and TalkType.objects.exists():\n            # Check for all talk types being disabled\n            can_submit = TalkType.objects.open_for_submission().count() > 0\n        context['can_submit'] = can_submit\n        return context\n\n    @revisions.create_revision()\n    def form_valid(self, form):\n        if not getattr(settings, 'WAFER_TALKS_OPEN', True):\n            # Should this be SuspiciousOperation?\n            raise ValidationError(\"Talk submission isn't open\")\n        # Eaaargh we have to do the work of CreateView if we want to set values\n        # before saving\n        self.object = form.save(commit=False)\n        self.object.corresponding_author = self.request.user\n        self.object.save()\n        revisions.set_user(self.request.user)\n        revisions.set_comment(\"Talk Created\")\n        # Save the author information as well (many-to-many fun)\n        form.save_m2m()\n        return HttpResponseRedirect(self.get_success_url())\n\n\nclass TalkUpdate(EditOwnTalksMixin, UpdateView):\n    model = Talk\n    template_name = 'wafer.talks/talk_form.html'\n\n    def get_form_class(self):\n        return get_talk_form_class()\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs['user'] = self.request.user\n        return kwargs\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context['can_edit'] = self.object.can_edit(self.request.user)\n        return context\n\n    @revisions.create_revision()\n    def form_valid(self, form):\n        revisions.set_user(self.request.user)\n        revisions.set_comment(\"Talk Modified\")\n        return super().form_valid(form)\n\n\nclass TalkWithdraw(EditOwnTalksMixin, DeleteView):\n    model = Talk\n    template_name = 'wafer.talks/talk_withdraw.html'\n    success_url = reverse_lazy('wafer_page')\n\n    @revisions.create_revision()\n    def delete(self, request, *args, **kwargs):\n        \"\"\"Override delete to only withdraw\"\"\"\n        talk = self.get_object()\n        talk.status = WITHDRAWN\n        talk.save()\n        revisions.set_user(self.request.user)\n        revisions.set_comment(\"Talk Withdrawn\")\n        return HttpResponseRedirect(self.success_url)\n\n\nclass TalkReview(PermissionRequiredMixin, CreateView):\n    model = Review\n    form_class = ReviewForm\n    permission_required = 'talks.add_review'\n    template_name = 'wafer.talks/review_talk.html'\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs['talk'] = Talk.objects.get(pk=self.kwargs['pk'])\n        kwargs['instance'] = self.get_object()\n        kwargs['user'] = self.request.user\n        return kwargs\n\n    def get_object(self):\n        try:\n            return Review.objects.get(\n                talk_id=self.kwargs['pk'], reviewer=self.request.user)\n        except Review.DoesNotExist:\n            return None\n\n    def form_valid(self, form):\n        existing = self.get_object()\n        response = super().form_valid(form)\n\n        review = self.get_object()\n        # Update the talk to 'under consideration' if a review is\n        # added.\n        talk = review.talk\n        if talk.status == SUBMITTED:\n            talk.status = UNDER_CONSIDERATION\n            with revisions.create_revision():\n                revisions.set_user(self.request.user)\n                revisions.set_comment(\"Status changed by review process\")\n                talk.save()\n\n        # Create the revision\n        # Note that we do this after the review has been saved\n        # (without a revision) in the super().form_valid call and\n        # after the scores have been added, so that the object_repr\n        # is correct\n        # We also do this after potentially updating the talk, so\n        # that the review revision time is correct for is_current\n        with revisions.create_revision():\n            revisions.set_user(self.request.user)\n            if existing:\n                revisions.set_comment(\"Review Modified\")\n            else:\n                revisions.set_comment(\"Review Created\")\n            review.save()\n\n        return response\n\n    def get_success_url(self):\n        return self.get_object().talk.get_absolute_url()\n\n\nclass Speakers(BuildableListView):\n    model = Talk\n    template_name = 'wafer.talks/speakers.html'\n    build_path = 'talks/speakers/index.html'\n\n    def _by_row(self, speakers, n):\n        return [speakers[i:i + n] for i in range(0, len(speakers), n)]\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        speakers = UserProfile.objects.filter(\n            user__talks__status='A').distinct().prefetch_related(\n            'user').order_by('user__talks__talk_type',\n                             'user__first_name',\n                             'user__last_name',\n                             'user__username').annotate(\n            talk_type=F('user__talks__talk_type__name'),\n            show_speakers=F('user__talks__talk_type__show_speakers'))\n        bytype = groupby(speakers, lambda x: x.talk_type)\n        context['speaker_rows'] = {}\n        for talk_type, type_speakers in bytype:\n            type_speakers = list(type_speakers)\n            # We explicitly check for False, as no talk type will give us None for\n            # show_speakers and we want to default to including that\n            if type_speakers and type_speakers[0].show_speakers is not False:\n                context[\"speaker_rows\"][talk_type] = self._by_row(type_speakers, 4)\n        return context\n\n\nclass TracksView(BuildableListView):\n    model = Track\n    template_name = 'wafer.talks/talk_tracks.html'\n    build_path = 'talks/tracks/index.html'\n\n\nclass TalkTypesView(BuildableListView):\n    model = TalkType\n    template_name = 'wafer.talks/talk_types.html'\n    build_path = 'talks/types/index.html'\n\n\nclass TalksViewSet(NestedViewSetMixin, viewsets.ModelViewSet):\n    \"\"\"API endpoint that allows talks to be viewed or edited.\"\"\"\n    queryset = Talk.objects.none()  # Needed for the REST Permissions\n    serializer_class = TalkSerializer\n    # XXX: Do we want to allow authors to edit talks via the API?\n    permission_classes = (DjangoModelPermissionsOrAnonReadOnly, )\n\n    @order_results_by('talk_type', 'talk_id')\n    def get_queryset(self):\n        # We override the default implementation to only show accepted talks\n        # to people who aren't part of the management group\n        if self.request.user.id is None:\n            # Anonymous user, so just accepted or cancelled talks\n            return Talk.objects.filter(Q(status=ACCEPTED) |\n                                       Q(status=CANCELLED))\n        elif Talk.can_view_all(self.request.user):\n            return Talk.objects.all()\n        else:\n            # Also include talks owned by the user\n            # XXX: Should this be all authors rather than just\n            # the corresponding author?\n            return Talk.objects.filter(\n                Q(status=ACCEPTED) |\n                Q(status=CANCELLED) |\n                Q(corresponding_author=self.request.user))\n\n\nclass TalkExistsPermission(BasePermission):\n    def has_permission(self, request, view):\n        talk_id = view.get_parents_query_dict()['talk']\n        if not Talk.objects.filter(pk=talk_id).exists():\n            raise Http404\n        return True\n\n\nclass TalkUrlsViewSet(NestedViewSetMixin, viewsets.ModelViewSet):\n    \"\"\"API endpoint that allows talks to be viewed or edited.\"\"\"\n    queryset = TalkUrl.objects.all().order_by('id')\n    serializer_class = TalkUrlSerializer\n    permission_classes = (DjangoModelPermissions, TalkExistsPermission)\n\n    def create(self, request, *args, **kw):\n        request.data['talk'] = self.get_parents_query_dict()['talk']\n        return super().create(request, *args, **kw)\n\n    def update(self, request, *args, **kw):\n        request.data['talk'] = self.get_parents_query_dict()['talk']\n        return super().update(request, *args, **kw)\n", "path": "wafer/talks/views.py"}], "after_files": [{"content": "from itertools import groupby\n\nfrom django.conf import settings\nfrom django.contrib.auth.mixins import (\n    LoginRequiredMixin, PermissionRequiredMixin)\nfrom django.contrib.auth.models import AnonymousUser\nfrom django.core.exceptions import PermissionDenied, ValidationError\nfrom django.db.models import Q, F\nfrom django.http import Http404\nfrom django.http import HttpResponseRedirect\nfrom django.urls import reverse_lazy\nfrom django.views.generic.edit import CreateView, UpdateView, DeleteView\n\nfrom bakery.views import BuildableDetailView, BuildableListView\nfrom rest_framework import viewsets\nfrom rest_framework.permissions import (\n    DjangoModelPermissions, DjangoModelPermissionsOrAnonReadOnly,\n    BasePermission)\nfrom rest_framework_extensions.mixins import NestedViewSetMixin\nfrom reversion import revisions\nfrom reversion.models import Version\n\nfrom wafer.talks.models import (\n    Review, Talk, TalkType, TalkUrl, Track,\n    ACCEPTED, CANCELLED, SUBMITTED, UNDER_CONSIDERATION, WITHDRAWN)\nfrom wafer.talks.forms import ReviewForm, get_talk_form_class\nfrom wafer.talks.serializers import TalkSerializer, TalkUrlSerializer\nfrom wafer.users.models import UserProfile\nfrom wafer.utils import order_results_by, PaginatedBuildableListView\n\n\nclass EditOwnTalksMixin(object):\n    '''Users can edit their own talks as long as the talk is\n       \"Under Consideration\"'''\n    def get_object(self, *args, **kwargs):\n        object_ = super().get_object(*args, **kwargs)\n        if object_.can_edit(self.request.user):\n            return object_\n        else:\n            raise PermissionDenied\n\n\nclass UsersTalks(PaginatedBuildableListView):\n    template_name = 'wafer.talks/talks.html'\n    build_prefix = 'talks'\n    paginate_by = 100\n\n    @order_results_by('talk_type', 'talk_id')\n    def get_queryset(self):\n        # self.request will be None when we come here via the static site\n        # renderer\n        if self.request and Talk.can_view_all(self.request.user):\n            talks = Talk.objects.all()\n        else:\n            talks = Talk.objects.filter(Q(status=ACCEPTED) | Q(status=CANCELLED))\n        return talks.prefetch_related(\n            \"talk_type\", \"corresponding_author\", \"authors\", \"authors__userprofile\"\n        )\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context[\"languages\"] = Talk.LANGUAGES\n        context[\"see_all\"] = Talk.can_view_all(self.request.user)\n        return context\n\n\nclass TalkView(BuildableDetailView):\n    template_name = 'wafer.talks/talk.html'\n    model = Talk\n\n    # Needed so django-bakery only renders public talks\n    def build_object(self, obj):\n        \"\"\"Override django-bakery to skip talks that raise 403\"\"\"\n        try:\n            super().build_object(obj)\n        except PermissionDenied:\n            # We cleanup the directory created\n            self.unbuild_object(obj)\n\n    def create_request(self, path):\n        request = super().create_request(path)\n        request.user = AnonymousUser()\n        return request\n\n    def get_object(self, *args, **kwargs):\n        '''Only talk owners can see talks, unless they've been accepted'''\n        object_ = super().get_object(*args, **kwargs)\n        if not object_.can_view(self.request.user):\n            raise PermissionDenied\n        return object_\n\n    def canonical_url(self):\n        '''Return the canonical URL for this view'''\n        return self.object.get_absolute_url()\n\n    def render_to_response(self, *args, **kwargs):\n        '''Canonicalize the URL if the slug changed'''\n        canonical_url = self.canonical_url()\n        if self.request.path != canonical_url:\n            return HttpResponseRedirect(canonical_url)\n        return super().render_to_response(*args, **kwargs)\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        talk = self.object\n        user = self.request.user\n\n        context['can_edit'] = talk.can_edit(user)\n\n        can_review = talk.can_review(user)\n        context['can_review'] = can_review\n        if can_review:\n            review = talk.reviews.filter(reviewer=user).first()\n            context['review_form'] = ReviewForm(\n                instance=review, talk=talk, user=user)\n\n        return context\n\n\nclass TalkCreate(LoginRequiredMixin, CreateView):\n    model = Talk\n    template_name = 'wafer.talks/talk_form.html'\n\n    def get_form_class(self):\n        return get_talk_form_class()\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs['user'] = self.request.user\n        return kwargs\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        can_submit = getattr(settings, 'WAFER_TALKS_OPEN', True)\n        if can_submit and TalkType.objects.exists():\n            # Check for all talk types being disabled\n            can_submit = TalkType.objects.open_for_submission().count() > 0\n        context['can_submit'] = can_submit\n        return context\n\n    @revisions.create_revision()\n    def form_valid(self, form):\n        if not getattr(settings, 'WAFER_TALKS_OPEN', True):\n            # Should this be SuspiciousOperation?\n            raise ValidationError(\"Talk submission isn't open\")\n        # Eaaargh we have to do the work of CreateView if we want to set values\n        # before saving\n        self.object = form.save(commit=False)\n        self.object.corresponding_author = self.request.user\n        self.object.save()\n        revisions.set_user(self.request.user)\n        revisions.set_comment(\"Talk Created\")\n        # Save the author information as well (many-to-many fun)\n        form.save_m2m()\n        return HttpResponseRedirect(self.get_success_url())\n\n\nclass TalkUpdate(EditOwnTalksMixin, UpdateView):\n    model = Talk\n    template_name = 'wafer.talks/talk_form.html'\n\n    def get_form_class(self):\n        return get_talk_form_class()\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs['user'] = self.request.user\n        return kwargs\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context['can_edit'] = self.object.can_edit(self.request.user)\n        return context\n\n    @revisions.create_revision()\n    def form_valid(self, form):\n        revisions.set_user(self.request.user)\n        revisions.set_comment(\"Talk Modified\")\n        return super().form_valid(form)\n\n\nclass TalkWithdraw(EditOwnTalksMixin, DeleteView):\n    model = Talk\n    template_name = 'wafer.talks/talk_withdraw.html'\n    success_url = reverse_lazy('wafer_page')\n\n\n    @revisions.create_revision()\n    def withdraw_helper(self, request):\n        \"\"\"Handle the logic for withdrawing a talk\"\"\"\n        talk = self.get_object()\n        talk.status = WITHDRAWN\n        talk.save()\n        revisions.set_user(self.request.user)\n        revisions.set_comment(\"Talk Withdrawn\")\n        return HttpResponseRedirect(self.success_url)\n\n    def delete(self, request, *args, **kwargs):\n        \"\"\"Override delete to only withdraw for Django < 4\"\"\"\n        return self.withdraw_helper(request)\n\n    def form_valid(self, request, *args, **kwargs):\n        \"\"\"Override delete to only withdraw for Django >= 4.0\"\"\"\n        return self.withdraw_helper(request)\n\n\nclass TalkReview(PermissionRequiredMixin, CreateView):\n    model = Review\n    form_class = ReviewForm\n    permission_required = 'talks.add_review'\n    template_name = 'wafer.talks/review_talk.html'\n\n    def get_form_kwargs(self):\n        kwargs = super().get_form_kwargs()\n        kwargs['talk'] = Talk.objects.get(pk=self.kwargs['pk'])\n        kwargs['instance'] = self.get_object()\n        kwargs['user'] = self.request.user\n        return kwargs\n\n    def get_object(self):\n        try:\n            return Review.objects.get(\n                talk_id=self.kwargs['pk'], reviewer=self.request.user)\n        except Review.DoesNotExist:\n            return None\n\n    def form_valid(self, form):\n        existing = self.get_object()\n        response = super().form_valid(form)\n\n        review = self.get_object()\n        # Update the talk to 'under consideration' if a review is\n        # added.\n        talk = review.talk\n        if talk.status == SUBMITTED:\n            talk.status = UNDER_CONSIDERATION\n            with revisions.create_revision():\n                revisions.set_user(self.request.user)\n                revisions.set_comment(\"Status changed by review process\")\n                talk.save()\n\n        # Create the revision\n        # Note that we do this after the review has been saved\n        # (without a revision) in the super().form_valid call and\n        # after the scores have been added, so that the object_repr\n        # is correct\n        # We also do this after potentially updating the talk, so\n        # that the review revision time is correct for is_current\n        with revisions.create_revision():\n            revisions.set_user(self.request.user)\n            if existing:\n                revisions.set_comment(\"Review Modified\")\n            else:\n                revisions.set_comment(\"Review Created\")\n            review.save()\n\n        return response\n\n    def get_success_url(self):\n        return self.get_object().talk.get_absolute_url()\n\n\nclass Speakers(BuildableListView):\n    model = Talk\n    template_name = 'wafer.talks/speakers.html'\n    build_path = 'talks/speakers/index.html'\n\n    def _by_row(self, speakers, n):\n        return [speakers[i:i + n] for i in range(0, len(speakers), n)]\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        speakers = UserProfile.objects.filter(\n            user__talks__status='A').distinct().prefetch_related(\n            'user').order_by('user__talks__talk_type',\n                             'user__first_name',\n                             'user__last_name',\n                             'user__username').annotate(\n            talk_type=F('user__talks__talk_type__name'),\n            show_speakers=F('user__talks__talk_type__show_speakers'))\n        bytype = groupby(speakers, lambda x: x.talk_type)\n        context['speaker_rows'] = {}\n        for talk_type, type_speakers in bytype:\n            type_speakers = list(type_speakers)\n            # We explicitly check for False, as no talk type will give us None for\n            # show_speakers and we want to default to including that\n            if type_speakers and type_speakers[0].show_speakers is not False:\n                context[\"speaker_rows\"][talk_type] = self._by_row(type_speakers, 4)\n        return context\n\n\nclass TracksView(BuildableListView):\n    model = Track\n    template_name = 'wafer.talks/talk_tracks.html'\n    build_path = 'talks/tracks/index.html'\n\n\nclass TalkTypesView(BuildableListView):\n    model = TalkType\n    template_name = 'wafer.talks/talk_types.html'\n    build_path = 'talks/types/index.html'\n\n\nclass TalksViewSet(NestedViewSetMixin, viewsets.ModelViewSet):\n    \"\"\"API endpoint that allows talks to be viewed or edited.\"\"\"\n    queryset = Talk.objects.none()  # Needed for the REST Permissions\n    serializer_class = TalkSerializer\n    # XXX: Do we want to allow authors to edit talks via the API?\n    permission_classes = (DjangoModelPermissionsOrAnonReadOnly, )\n\n    @order_results_by('talk_type', 'talk_id')\n    def get_queryset(self):\n        # We override the default implementation to only show accepted talks\n        # to people who aren't part of the management group\n        if self.request.user.id is None:\n            # Anonymous user, so just accepted or cancelled talks\n            return Talk.objects.filter(Q(status=ACCEPTED) |\n                                       Q(status=CANCELLED))\n        elif Talk.can_view_all(self.request.user):\n            return Talk.objects.all()\n        else:\n            # Also include talks owned by the user\n            # XXX: Should this be all authors rather than just\n            # the corresponding author?\n            return Talk.objects.filter(\n                Q(status=ACCEPTED) |\n                Q(status=CANCELLED) |\n                Q(corresponding_author=self.request.user))\n\n\nclass TalkExistsPermission(BasePermission):\n    def has_permission(self, request, view):\n        talk_id = view.get_parents_query_dict()['talk']\n        if not Talk.objects.filter(pk=talk_id).exists():\n            raise Http404\n        return True\n\n\nclass TalkUrlsViewSet(NestedViewSetMixin, viewsets.ModelViewSet):\n    \"\"\"API endpoint that allows talks to be viewed or edited.\"\"\"\n    queryset = TalkUrl.objects.all().order_by('id')\n    serializer_class = TalkUrlSerializer\n    permission_classes = (DjangoModelPermissions, TalkExistsPermission)\n\n    def create(self, request, *args, **kw):\n        request.data['talk'] = self.get_parents_query_dict()['talk']\n        return super().create(request, *args, **kw)\n\n    def update(self, request, *args, **kw):\n        request.data['talk'] = self.get_parents_query_dict()['talk']\n        return super().update(request, *args, **kw)\n", "path": "wafer/talks/views.py"}]}