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 %}

templates/sponsors/admin/sponsorship_change_form.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
</li>
2424
{% endif %}
2525

26+
{% if sp.CANCELLED in sp.next_status %}
27+
<li>
28+
<a href="{% url 'admin:sponsors_sponsorship_cancel' sp.pk %}" style="background: #d9534f">Cancel</a>
29+
</li>
30+
{% endif %}
31+
2632
{% if sp.FINALIZED in sp.next_status %}
2733
<li>
2834
<a href="{{ sp.contract_admin_url }}" style="background: #417690">Review Contract</a>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
content email [email protected]
2+
3+
CANCELLED SPONSORSHIP
4+
5+
{{ sponsorship }}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CANCELLED SPONSORSHIP to [email protected]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
content email sponsors
2+
3+
CANCELLED SPONSORSHIP
4+
5+
{{ sponsorship }}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CANCELLED SPONSORSHIP subject email user + verified emails

0 commit comments

Comments
 (0)