Repository: CTPUG/wafer

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.


Discussion:

Resolving patch:
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 @@ class TalkWithdraw(EditOwnTalksMixin, DeleteView):
     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 @@ def delete(self, request, *args, **kwargs):
         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


Test patch:
diff --git a/wafer/talks/tests/test_views.py b/wafer/talks/tests/test_views.py
--- a/wafer/talks/tests/test_views.py
+++ b/wafer/talks/tests/test_views.py
@@ -9,7 +9,7 @@
 from wafer.tests.utils import create_user, mock_avatar_url
 from wafer.talks.models import (
     Talk, TalkUrl, ACCEPTED, REJECTED, SUBMITTED, UNDER_CONSIDERATION,
-    CANCELLED, PROVISIONAL)
+    CANCELLED, PROVISIONAL, WITHDRAWN)
 from wafer.talks.tests.fixtures import create_talk, create_talk_type
 
 
@@ -158,6 +158,66 @@ def test_view_canonicalizes_url_correctly(self):
             follow=True)
         self.assertEqual(response.status_code, 200)
 
+class TalkDeleteViewTests(TestCase):
+    def setUp(self):
+        self.talk_a = create_talk("Talk A", ACCEPTED, "author_a")
+        self.talk_s = create_talk("Talk S", SUBMITTED, "author_s")
+        self.talk_u = create_talk("Talk U", UNDER_CONSIDERATION, "author_u")
+        self.talk_p = create_talk("Talk P", PROVISIONAL, "author_p")
+        self.client = Client()
+
+    def test_delete_permissions_accepted(self):
+        self.client.login(username='author_a', password='author_a_password')
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_a.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_permissions_provisionally_accepted(self):
+        self.client.login(username='author_p', password='author_p_password')
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_p.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_other_talks_provisional(self):
+        self.client.login(username='author_p', password='author_p_password')
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_a.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_s.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_u.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_other_talks_submitted(self):
+        self.client.login(username='author_s', password='author_s_password')
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_a.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_p.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_u.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 403)
+
+    def test_delete_is_withdraw_under_consideration(self):
+        self.client.login(username='author_u', password='author_u_password')
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_u.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 302)
+        self.talk_u.refresh_from_db()
+        self.assertEqual(self.talk_u.status, WITHDRAWN)
+
+    def test_delete_is_withdraw_submitted(self):
+        self.client.login(username='author_s', password='author_s_password')
+        post_url = reverse('wafer_talk_withdraw', kwargs={'pk': self.talk_s.pk})
+        response = self.client.post(post_url)
+        self.assertEqual(response.status_code, 302)
+        self.talk_s.refresh_from_db()
+        self.assertEqual(self.talk_s.status, WITHDRAWN)
+
 
 class TalkNoteViewTests(TestCase):
     def setUp(self):