diff --git a/sponsors/admin.py b/sponsors/admin.py index 405ad9cbe..d49cf3cea 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -244,6 +244,11 @@ def get_urls(self): self.admin_site.admin_view(self.approve_sponsorship_view), name="sponsors_sponsorship_approve", ), + path( + "/enable-edit", + self.admin_site.admin_view(self.rollback_to_editing_view), + name="sponsors_sponsorship_rollback_to_edit", + ), ] return my_urls + urls @@ -322,6 +327,31 @@ def get_sponsor_contacts(self, obj): get_sponsor_contacts.short_description = "Contacts" + def rollback_to_editing_view(self, request, pk): + sponsorship = get_object_or_404(self.get_queryset(request), pk=pk) + + if request.method.upper() == "POST" and request.POST.get("confirm") == "yes": + try: + sponsorship.rollback_to_editing() + sponsorship.save() + self.message_user( + request, "Sponsorship is now editable!", messages.SUCCESS + ) + except SponsorshipInvalidStatusException as e: + self.message_user(request, str(e), messages.ERROR) + + redirect_url = reverse( + "admin:sponsors_sponsorship_change", args=[sponsorship.pk] + ) + return redirect(redirect_url) + + context = {"sponsorship": sponsorship} + return render( + request, + "sponsors/admin/rollback_sponsorship_to_editing.html", + context=context, + ) + def reject_sponsorship_view(self, request, pk): sponsorship = get_object_or_404(self.get_queryset(request), pk=pk) diff --git a/sponsors/models.py b/sponsors/models.py index d0a90cbe5..8594e372c 100644 --- a/sponsors/models.py +++ b/sponsors/models.py @@ -342,6 +342,15 @@ def approve(self): self.status = self.APPROVED self.approved_on = timezone.now().date() + def rollback_to_editing(self): + accepts_rollback = [self.APPLIED, self.APPROVED, self.REJECTED] + if self.status not in accepts_rollback: + msg = f"Can't rollback to edit a {self.get_status_display()} sponsorship." + raise SponsorshipInvalidStatusException(msg) + self.status = self.APPLIED + self.approved_on = None + self.rejected_on = None + @property def verified_emails(self): emails = [self.submited_by.email] diff --git a/sponsors/tests/test_models.py b/sponsors/tests/test_models.py index e18f8a111..53aab55e7 100644 --- a/sponsors/tests/test_models.py +++ b/sponsors/tests/test_models.py @@ -6,7 +6,10 @@ from django.utils import timezone from ..models import Sponsor, SponsorshipBenefit, Sponsorship -from ..exceptions import SponsorWithExistingApplicationException +from ..exceptions import ( + SponsorWithExistingApplicationException, + SponsorshipInvalidStatusException, +) class SponsorshipBenefitModelTests(TestCase): @@ -142,6 +145,30 @@ def test_approve_sponsorship(self): self.assertEqual(sponsorship.status, Sponsorship.APPROVED) self.assertEqual(sponsorship.approved_on, timezone.now().date()) + def test_rollback_sponsorship_to_edit(self): + sponsorship = Sponsorship.new(self.sponsor, self.benefits) + can_rollback_from = [ + Sponsorship.APPLIED, + Sponsorship.APPROVED, + Sponsorship.REJECTED, + ] + for status in can_rollback_from: + sponsorship.status = status + sponsorship.save() + sponsorship.refresh_from_db() + + sponsorship.rollback_to_editing() + + self.assertEqual(sponsorship.status, Sponsorship.APPLIED) + self.assertIsNone(sponsorship.approved_on) + self.assertIsNone(sponsorship.rejected_on) + + sponsorship.status = Sponsorship.FINALIZED + sponsorship.save() + sponsorship.refresh_from_db() + with self.assertRaises(SponsorshipInvalidStatusException): + sponsorship.rollback_to_editing() + def test_raise_exception_when_trying_to_create_sponsorship_for_same_sponsor(self): sponsorship = Sponsorship.new(self.sponsor, self.benefits) finalized_status = [Sponsorship.REJECTED, Sponsorship.FINALIZED] diff --git a/sponsors/tests/test_views.py b/sponsors/tests/test_views.py index 7641f4d2f..99bf18cef 100644 --- a/sponsors/tests/test_views.py +++ b/sponsors/tests/test_views.py @@ -321,6 +321,108 @@ def test_redirect_user_back_to_benefits_selection_if_post_without_valid_set_of_b self.assertRedirects(r, reverse("select_sponsorship_application_benefits")) +class RollbackSponsorshipToEditingAdminViewTests(TestCase): + def setUp(self): + self.user = baker.make( + settings.AUTH_USER_MODEL, is_staff=True, is_superuser=True + ) + self.client.force_login(self.user) + self.sponsorship = baker.make( + Sponsorship, + status=Sponsorship.APPROVED, + submited_by=self.user, + _fill_optional=True, + ) + self.url = reverse( + "admin:sponsors_sponsorship_rollback_to_edit", args=[self.sponsorship.pk] + ) + + def test_display_confirmation_form_on_get(self): + response = self.client.get(self.url) + context = response.context + self.sponsorship.refresh_from_db() + + self.assertTemplateUsed( + response, "sponsors/admin/rollback_sponsorship_to_editing.html" + ) + self.assertEqual(context["sponsorship"], self.sponsorship) + self.assertNotEqual( + self.sponsorship.status, Sponsorship.APPLIED + ) # did not update + + def test_rollback_sponsorship_to_applied_on_post(self): + data = {"confirm": "yes"} + response = self.client.post(self.url, data=data) + self.sponsorship.refresh_from_db() + + expected_url = reverse( + "admin:sponsors_sponsorship_change", args=[self.sponsorship.pk] + ) + self.assertRedirects(response, expected_url, fetch_redirect_response=True) + self.assertEqual(self.sponsorship.status, Sponsorship.APPLIED) + msg = list(get_messages(response.wsgi_request))[0] + assertMessage(msg, "Sponsorship is now editable!", messages.SUCCESS) + + def test_do_not_rollback_if_invalid_post(self): + response = self.client.post(self.url, data={}) + self.sponsorship.refresh_from_db() + self.assertTemplateUsed( + response, "sponsors/admin/rollback_sponsorship_to_editing.html" + ) + self.assertNotEqual( + self.sponsorship.status, Sponsorship.APPLIED + ) # did not update + + response = self.client.post(self.url, data={"confirm": "invalid"}) + self.sponsorship.refresh_from_db() + self.assertTemplateUsed( + response, "sponsors/admin/rollback_sponsorship_to_editing.html" + ) + self.assertNotEqual(self.sponsorship.status, Sponsorship.APPLIED) + + def test_404_if_sponsorship_does_not_exist(self): + self.sponsorship.delete() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) + + def test_login_required(self): + login_url = reverse("admin:login") + redirect_url = f"{login_url}?next={self.url}" + self.client.logout() + + r = self.client.get(self.url) + + self.assertRedirects(r, redirect_url) + + def test_staff_required(self): + login_url = reverse("admin:login") + redirect_url = f"{login_url}?next={self.url}" + self.user.is_staff = False + self.user.save() + self.client.force_login(self.user) + + r = self.client.get(self.url) + + self.assertRedirects(r, redirect_url, fetch_redirect_response=False) + + def test_message_user_if_rejecting_invalid_sponsorship(self): + self.sponsorship.status = Sponsorship.FINALIZED + self.sponsorship.save() + data = {"confirm": "yes"} + response = self.client.post(self.url, data=data) + self.sponsorship.refresh_from_db() + + expected_url = reverse( + "admin:sponsors_sponsorship_change", args=[self.sponsorship.pk] + ) + self.assertRedirects(response, expected_url, fetch_redirect_response=True) + self.assertEqual(self.sponsorship.status, Sponsorship.FINALIZED) + msg = list(get_messages(response.wsgi_request))[0] + assertMessage( + msg, "Can't rollback to edit a Finalized sponsorship.", messages.ERROR + ) + + class RejectedSponsorshipAdminViewTests(TestCase): def setUp(self): self.user = baker.make( diff --git a/templates/sponsors/admin/rollback_sponsorship_to_editing.html b/templates/sponsors/admin/rollback_sponsorship_to_editing.html new file mode 100644 index 000000000..29d157495 --- /dev/null +++ b/templates/sponsors/admin/rollback_sponsorship_to_editing.html @@ -0,0 +1,35 @@ +{% extends 'admin/base_site.html' %} +{% load i18n admin_static sponsors %} + +{% block extrastyle %}{{ block.super }}{% endblock %} + +{% block title %}Rollback {{ sponsorship }} to editing| python.org{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

Rollback to Editing

+

Please review the sponsorship application and click in the Rollback button if you want to proceed.

+
+
+{% csrf_token %} + +
{% full_sponsorship sponsorship display_fee=True %}
+ + + +
+ +
+ +
+
+
{% endblock %} diff --git a/templates/sponsors/admin/sponsorship_change_form.html b/templates/sponsors/admin/sponsorship_change_form.html index 5a983f31a..c366bd081 100644 --- a/templates/sponsors/admin/sponsorship_change_form.html +++ b/templates/sponsors/admin/sponsorship_change_form.html @@ -16,6 +16,12 @@ {% endif %} + {% if sp.status != sp.FINALIZED and sp.status != sp.APPLIED %} +
  • + Rollback to Edit +
  • + {% endif %} + {% endwith %} {{ block.super }}