Skip to content

Commit 4aba973

Browse files
committed
feat: add new cancelled status for sponsors
adds cancellation notifications, admin form buttons and page, state transitions, migration
1 parent 902fb39 commit 4aba973

15 files changed

+165
-7
lines changed

sponsors/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,11 @@ def get_urls(self):
588588
self.admin_site.admin_view(self.approve_sponsorship_view),
589589
name=f"{base_name}_approve",
590590
),
591+
path(
592+
"<int:pk>/cancel",
593+
self.admin_site.admin_view(self.cancel_sponsorship_view),
594+
name=f"{base_name}_cancel",
595+
),
591596
path(
592597
"<int:pk>/enable-edit",
593598
self.admin_site.admin_view(self.rollback_to_editing_view),
@@ -745,6 +750,9 @@ def reject_sponsorship_view(self, request, pk):
745750
def approve_sponsorship_view(self, request, pk):
746751
return views_admin.approve_sponsorship_view(self, request, pk)
747752

753+
def cancel_sponsorship_view(self, request, pk):
754+
return views_admin.cancel_sponsorship_view(self, request, pk)
755+
748756
def approve_signed_sponsorship_view(self, request, pk):
749757
return views_admin.approve_signed_sponsorship_view(self, request, pk)
750758

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 4.2.22 on 2025-06-26 16:06
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('sponsors', '0103_alter_benefitfeature_polymorphic_ctype_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='sponsorship',
15+
name='cancelled_on',
16+
field=models.DateField(blank=True, null=True),
17+
),
18+
migrations.AlterField(
19+
model_name='sponsorship',
20+
name='status',
21+
field=models.CharField(choices=[('applied', 'Applied'), ('rejected', 'Rejected'), ('approved', 'Approved'), ('finalized', 'Finalized'), ('cancelled', 'Cancelled')], db_index=True, default='applied', max_length=20),
22+
),
23+
]

sponsors/models/managers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def approved(self):
1717

1818
def visible_to(self, user):
1919
contacts = user.sponsorcontact_set.values_list('sponsor_id', flat=True)
20-
status = [self.model.APPLIED, self.model.APPROVED, self.model.FINALIZED]
20+
status = [self.model.APPLIED, self.model.APPROVED, self.model.FINALIZED, self.model.CANCELLED]
2121
return self.filter(
2222
Q(submited_by=user) | Q(sponsor_id__in=Subquery(contacts)),
2323
status__in=status,

sponsors/models/sponsorship.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,14 @@ class Sponsorship(models.Model):
156156
REJECTED = "rejected"
157157
APPROVED = "approved"
158158
FINALIZED = "finalized"
159+
CANCELLED = "cancelled"
159160

160161
STATUS_CHOICES = [
161162
(APPLIED, "Applied"),
162163
(REJECTED, "Rejected"),
163164
(APPROVED, "Approved"),
164165
(FINALIZED, "Finalized"),
166+
(CANCELLED, "Cancelled"),
165167
]
166168

167169
objects = SponsorshipQuerySet.as_manager()
@@ -180,6 +182,7 @@ class Sponsorship(models.Model):
180182
applied_on = models.DateField(auto_now_add=True)
181183
approved_on = models.DateField(null=True, blank=True)
182184
rejected_on = models.DateField(null=True, blank=True)
185+
cancelled_on = models.DateField(null=True, blank=True)
183186
finalized_on = models.DateField(null=True, blank=True)
184187
year = models.PositiveIntegerField(null=True, validators=YEAR_VALIDATORS, db_index=True)
185188

@@ -218,7 +221,14 @@ def level_name(self, value):
218221
@cached_property
219222
def user_customizations(self):
220223
benefits = [b.sponsorship_benefit for b in self.benefits.select_related("sponsorship_benefit")]
221-
return self.package.get_user_customization(benefits)
224+
if self.package:
225+
return self.package.get_user_customization(benefits)
226+
else:
227+
# Return default customization structure for sponsorships without packages
228+
return {
229+
"added_by_user": [],
230+
"removed_by_user": []
231+
}
222232

223233
def __str__(self):
224234
repr = f"{self.level_name} - {self.year} - ({self.get_status_display()}) for sponsor {self.sponsor.name}"
@@ -327,8 +337,17 @@ def approve(self, start_date, end_date):
327337
self.end_date = end_date
328338
self.approved_on = timezone.now().date()
329339

340+
341+
def cancel(self):
342+
if self.CANCELLED not in self.next_status:
343+
msg = f"Can't cancel a {self.get_status_display()} sponsorship."
344+
raise InvalidStatusException(msg)
345+
self.status = self.CANCELLED
346+
self.locked = True
347+
self.cancelled_on = timezone.now().date()
348+
330349
def rollback_to_editing(self):
331-
accepts_rollback = [self.APPLIED, self.APPROVED, self.REJECTED]
350+
accepts_rollback = [self.APPLIED, self.APPROVED, self.REJECTED, self.CANCELLED]
332351
if self.status not in accepts_rollback:
333352
msg = f"Can't rollback to edit a {self.get_status_display()} sponsorship."
334353
raise InvalidStatusException(msg)
@@ -345,6 +364,7 @@ def rollback_to_editing(self):
345364
self.status = self.APPLIED
346365
self.approved_on = None
347366
self.rejected_on = None
367+
self.cancelled_on = None
348368

349369
@property
350370
def unlocked(self):
@@ -388,10 +408,11 @@ def open_for_editing(self):
388408
@property
389409
def next_status(self):
390410
states_map = {
391-
self.APPLIED: [self.APPROVED, self.REJECTED],
392-
self.APPROVED: [self.FINALIZED],
411+
self.APPLIED: [self.APPROVED, self.REJECTED, self.CANCELLED],
412+
self.APPROVED: [self.FINALIZED, self.CANCELLED],
393413
self.REJECTED: [],
394414
self.FINALIZED: [],
415+
self.CANCELLED: [],
395416
}
396417
return states_map[self.status]
397418

sponsors/notifications.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ class RejectedSponsorshipNotificationToSponsors(BaseEmailSponsorshipNotification
8686
def get_recipient_list(self, context):
8787
return context["sponsorship"].verified_emails
8888

89+
class CancelledSponsorshipNotificationToPSF:
90+
subject_template = "sponsors/email/psf_cancelled_sponsorship_subject.txt"
91+
message_template = "sponsors/email/psf_cancelled_sponsorship.txt"
92+
email_context_keys = ["sponsorship"]
93+
94+
def get_recipient_list(self, context):
95+
return [settings.SPONSORSHIP_NOTIFICATION_TO_EMAIL]
96+
97+
class CancelledSponsorshipNotificationToSponsors(BaseEmailSponsorshipNotification):
98+
subject_template = "sponsors/email/sponsor_cancelled_sponsorship_subject.txt"
99+
message_template = "sponsors/email/sponsor_cancelled_sponsorship.txt"
100+
email_context_keys = ["sponsorship"]
101+
102+
def get_recipient_list(self, context):
103+
return context["sponsorship"].verified_emails
89104

90105
class ContractNotificationToPSF(BaseEmailSponsorshipNotification):
91106
subject_template = "sponsors/email/psf_contract_subject.txt"

sponsors/tests/test_admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def test_lookups(self):
3131
("rejected", "Rejected"),
3232
("approved", "Approved"),
3333
("finalized", "Finalized"),
34+
("cancelled", "Cancelled"),
3435
]
3536
self.assertEqual(expected, self.filter.lookups(self.request, self.model_admin))
3637

sponsors/tests/test_models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ def setUp(self):
101101

102102
def test_control_sponsorship_next_status(self):
103103
states_map = {
104-
Sponsorship.APPLIED: [Sponsorship.APPROVED, Sponsorship.REJECTED],
105-
Sponsorship.APPROVED: [Sponsorship.FINALIZED],
104+
Sponsorship.APPLIED: [Sponsorship.APPROVED, Sponsorship.REJECTED, Sponsorship.CANCELLED],
105+
Sponsorship.APPROVED: [Sponsorship.FINALIZED, Sponsorship.CANCELLED],
106106
Sponsorship.REJECTED: [],
107107
Sponsorship.FINALIZED: [],
108+
Sponsorship.CANCELLED: [],
108109
}
109110
for status, exepcted in states_map.items():
110111
sponsorship = baker.prepare(Sponsorship, status=status)

sponsors/use_cases.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ def execute(self, sponsorship, request=None):
4646
return sponsorship
4747

4848

49+
class CancelSponsorshipApplicationUseCase(BaseUseCaseWithNotifications):
50+
notifications = [
51+
notifications.CancelledSponsorshipNotificationToPSF(),
52+
notifications.CancelledSponsorshipNotificationToSponsors(),
53+
]
54+
55+
def execute(self, sponsorship, request=None):
56+
sponsorship.cancel()
57+
sponsorship.save()
58+
self.notify(request=request, sponsorship=sponsorship)
59+
return sponsorship
60+
61+
4962
class ApproveSponsorshipApplicationUseCase(BaseUseCaseWithNotifications):
5063
notifications = [
5164
notifications.SponsorshipApprovalLogger(),

sponsors/views_admin.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ def reject_sponsorship_view(ModelAdmin, request, pk):
5252
return render(request, "sponsors/admin/reject_application.html", context=context)
5353

5454

55+
56+
57+
def cancel_sponsorship_view(ModelAdmin, request, pk):
58+
sponsorship = get_object_or_404(ModelAdmin.get_queryset(request), pk=pk)
59+
60+
if request.method.upper() == "POST" and request.POST.get("confirm") == "yes":
61+
try:
62+
use_case = use_cases.CancelSponsorshipApplicationUseCase.build()
63+
use_case.execute(sponsorship)
64+
ModelAdmin.message_user(
65+
request, "Sponsorship was cancelled!", messages.SUCCESS
66+
)
67+
except InvalidStatusException as e:
68+
ModelAdmin.message_user(request, str(e), messages.ERROR)
69+
70+
redirect_url = reverse(
71+
"admin:sponsors_sponsorship_change", args=[sponsorship.pk]
72+
)
73+
return redirect(redirect_url)
74+
75+
context = {"sponsorship": sponsorship}
76+
return render(request, "sponsors/admin/cancel_application.html", context=context)
77+
78+
5579
def approve_sponsorship_view(ModelAdmin, request, pk):
5680
"""
5781
Approves a sponsorship and create an empty contract
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{% extends 'admin/base_site.html' %}
2+
{% load i18n static sponsors %}
3+
4+
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">{% endblock %}
5+
6+
{% block title %}Cancel {{ sponsorship }} | python.org{% endblock %}
7+
8+
{% block breadcrumbs %}
9+
<div class="breadcrumbs">
10+
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> &gt
11+
<a href="{% url 'admin:app_list' app_label='sponsors' %}">{% trans 'Sponsors' %}</a> &gt
12+
<a href="{% url 'admin:sponsors_sponsorship_changelist' %}">{% trans 'Sponsorship' %}</a> &gt
13+
<a href="{% url 'admin:sponsors_sponsorship_change' sponsorship.pk %}">{{ sponsorship }}</a> &gt
14+
{% trans 'Cancel' %}
15+
</div>
16+
{% endblock %}
17+
18+
{% block content %}
19+
<h1>Cancel Sponsorship</h1>
20+
<p>Please review the sponsorship application and click the Cancel button to mark it as cancelled by administration.</p>
21+
<div id="content-main">
22+
<form action="" method="post" id="cancel_sponsorship_form">
23+
{% csrf_token %}
24+
25+
<pre>{% full_sponsorship sponsorship display_fee=True %}</pre>
26+
27+
<input name="confirm" value="yes" style="display:none">
28+
29+
<div class="submit-row">
30+
<input type="submit" value="Cancel" class="default">
31+
</div>
32+
33+
</form>
34+
</div>{% endblock %}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy