Skip to content

Commit 4821ea2

Browse files
authored
Sponsorship review ux (#1684)
* Separate package benefits in application form * Flag sponsors benefits added by user * Split package and added benefits in full sponsorship detail * Filter by package only benefits * Enable Sponsorship's state control * Prevents from rejecting or accepting reviewd applications * Add buttons to approve/reject sponsorship application * Implement HTML to reject/approve sponsorship * Improve admin * Display fee when rejecting/approving * Fix breaking tests * Display detailed sponsor's address information * Should appennd, not overwrite HTML snippet * Fix f-string and change postal code order * Add titles to approve/reject html * Better display sponsorship
1 parent a359155 commit 4821ea2

File tree

12 files changed

+254
-21
lines changed

12 files changed

+254
-21
lines changed

sponsors/admin.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
)
2020
from sponsors import use_cases
2121
from sponsors.forms import SponsorshipReviewAdminForm
22+
from sponsors.exceptions import SponsorshipInvalidStatusException
2223
from cms.admin import ContentManageableModelAdmin
2324

2425

@@ -101,16 +102,19 @@ class SponsorBenefitInline(admin.TabularInline):
101102

102103
@admin.register(Sponsorship)
103104
class SponsorshipAdmin(admin.ModelAdmin):
105+
change_form_template = "sponsors/admin/sponsorship_change_form.html"
104106
form = SponsorshipReviewAdminForm
105107
inlines = [SponsorBenefitInline]
106108
list_display = [
107109
"sponsor",
110+
"status",
108111
"applied_on",
109112
"approved_on",
110113
"start_date",
111114
"end_date",
112115
"display_sponsorship_link",
113116
]
117+
list_filter = ["status"]
114118
readonly_fields = [
115119
"for_modified_package",
116120
"sponsor",
@@ -260,7 +264,19 @@ def get_sponsor_primary_phone(self, obj):
260264
get_sponsor_primary_phone.short_description = "Primary Phone"
261265

262266
def get_sponsor_mailing_address(self, obj):
263-
return obj.sponsor.mailing_address
267+
sponsor = obj.sponsor
268+
city_row = f'{sponsor.city} - {sponsor.get_country_display()} ({sponsor.country})'
269+
if sponsor.state:
270+
city_row = f'{sponsor.city} - {sponsor.state} - {sponsor.get_country_display()} ({sponsor.country})'
271+
272+
mail_row = sponsor.mailing_address_line_1
273+
if sponsor.mailing_address_line_2:
274+
mail_row += f' - {sponsor.mailing_address_line_2}'
275+
276+
html = f'<p>{city_row}</p>'
277+
html += f'<p>{mail_row}</p>'
278+
html += f'<p>{sponsor.postal_code}</p>'
279+
return mark_safe(html)
264280

265281
get_sponsor_mailing_address.short_description = "Mailing/Billing Address"
266282

@@ -276,7 +292,7 @@ def get_sponsor_contacts(self, obj):
276292
)
277293
html += "</ul>"
278294
if not_primary:
279-
html = "<b>Other contacts</b><ul>"
295+
html += "<b>Other contacts</b><ul>"
280296
html += "".join(
281297
[f"<li>{c.name}: {c.email} / {c.phone}</li>" for c in not_primary]
282298
)
@@ -289,12 +305,18 @@ def reject_sponsorship_view(self, request, pk):
289305
sponsorship = get_object_or_404(self.get_queryset(request), pk=pk)
290306

291307
if request.method.upper() == "POST" and request.POST.get("confirm") == "yes":
292-
use_case = use_cases.RejectSponsorshipApplicationUseCase.build()
293-
use_case.execute(sponsorship)
308+
try:
309+
use_case = use_cases.RejectSponsorshipApplicationUseCase.build()
310+
use_case.execute(sponsorship)
311+
self.message_user(
312+
request, "Sponsorship was rejected!", messages.SUCCESS
313+
)
314+
except SponsorshipInvalidStatusException as e:
315+
self.message_user(request, str(e), messages.ERROR)
316+
294317
redirect_url = reverse(
295318
"admin:sponsors_sponsorship_change", args=[sponsorship.pk]
296319
)
297-
self.message_user(request, "Sponsorship was rejected!", messages.SUCCESS)
298320
return redirect(redirect_url)
299321

300322
context = {"sponsorship": sponsorship}
@@ -306,12 +328,18 @@ def approve_sponsorship_view(self, request, pk):
306328
sponsorship = get_object_or_404(self.get_queryset(request), pk=pk)
307329

308330
if request.method.upper() == "POST" and request.POST.get("confirm") == "yes":
309-
sponsorship.approve()
310-
sponsorship.save()
331+
try:
332+
sponsorship.approve()
333+
sponsorship.save()
334+
self.message_user(
335+
request, "Sponsorship was approved!", messages.SUCCESS
336+
)
337+
except SponsorshipInvalidStatusException as e:
338+
self.message_user(request, str(e), messages.ERROR)
339+
311340
redirect_url = reverse(
312341
"admin:sponsors_sponsorship_change", args=[sponsorship.pk]
313342
)
314-
self.message_user(request, "Sponsorship was approved!", messages.SUCCESS)
315343
return redirect(redirect_url)
316344

317345
context = {"sponsorship": sponsorship}

sponsors/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,10 @@ class SponsorWithExistingApplicationException(Exception):
33
Raised when user tries to create a new Sponsorship application
44
for a Sponsor which already has applications pending to review
55
"""
6+
7+
8+
class SponsorshipInvalidStatusException(Exception):
9+
"""
10+
Raised when user tries to change the Sponsorship's status
11+
to a new one but from an invalid current status
12+
"""

sponsors/models.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
from companies.models import Company
1616

1717
from .managers import SponsorshipQuerySet
18-
from .exceptions import SponsorWithExistingApplicationException
18+
from .exceptions import (
19+
SponsorWithExistingApplicationException,
20+
SponsorshipInvalidStatusException,
21+
)
1922

2023
DEFAULT_MARKUP_TYPE = getattr(settings, "DEFAULT_MARKUP_TYPE", "restructuredtext")
2124

@@ -267,6 +270,15 @@ class Sponsorship(models.Model):
267270
level_name = models.CharField(max_length=64, default="")
268271
sponsorship_fee = models.PositiveIntegerField(null=True, blank=True)
269272

273+
def __str__(self):
274+
repr = f"{self.level_name} ({self.get_status_display()}) for sponsor {self.sponsor.name}"
275+
if self.start_date and self.end_date:
276+
fmt = "%m/%d/%Y"
277+
start = self.start_date.strftime(fmt)
278+
end = self.end_date.strftime(fmt)
279+
repr += f" [{start} - {end}]"
280+
return repr
281+
270282
@classmethod
271283
def new(cls, sponsor, benefits, package=None, submited_by=None):
272284
"""
@@ -317,10 +329,16 @@ def estimated_cost(self):
317329
)
318330

319331
def reject(self):
332+
if self.REJECTED not in self.next_status:
333+
msg = f"Can't reject a {self.get_status_display()} sponsorship."
334+
raise SponsorshipInvalidStatusException(msg)
320335
self.status = self.REJECTED
321336
self.rejected_on = timezone.now().date()
322337

323338
def approve(self):
339+
if self.APPROVED not in self.next_status:
340+
msg = f"Can't approve a {self.get_status_display()} sponsorship."
341+
raise SponsorshipInvalidStatusException(msg)
324342
self.status = self.APPROVED
325343
self.approved_on = timezone.now().date()
326344

@@ -343,6 +361,16 @@ def package_benefits(self):
343361
def added_benefits(self):
344362
return self.benefits.filter(added_by_user=True)
345363

364+
@property
365+
def next_status(self):
366+
states_map = {
367+
self.APPLIED: [self.APPROVED, self.REJECTED],
368+
self.APPROVED: [self.FINALIZED],
369+
self.REJECTED: [],
370+
self.FINALIZED: [],
371+
}
372+
return states_map[self.status]
373+
346374

347375
class SponsorBenefit(models.Model):
348376
sponsorship = models.ForeignKey(

sponsors/templatetags/sponsors.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77

88

99
@register.inclusion_tag("sponsors/partials/full_sponsorship.txt")
10-
def full_sponsorship(sponsorship):
10+
def full_sponsorship(sponsorship, display_fee=False):
11+
if not display_fee:
12+
display_fee = not sponsorship.for_modified_package
1113
return {
1214
"sponsorship": sponsorship,
1315
"sponsor": sponsorship.sponsor,
1416
"benefits": list(sponsorship.benefits.all()),
17+
"display_fee": display_fee,
1518
}

sponsors/tests/test_models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ def setUp(self):
4141
self.sponsor = baker.make("sponsors.Sponsor")
4242
self.user = baker.make(settings.AUTH_USER_MODEL)
4343

44+
def test_control_sponsorship_next_status(self):
45+
states_map = {
46+
Sponsorship.APPLIED: [Sponsorship.APPROVED, Sponsorship.REJECTED],
47+
Sponsorship.APPROVED: [Sponsorship.FINALIZED],
48+
Sponsorship.REJECTED: [],
49+
Sponsorship.FINALIZED: [],
50+
}
51+
for status, exepcted in states_map.items():
52+
sponsorship = baker.prepare(Sponsorship, status=status)
53+
self.assertEqual(sponsorship.next_status, exepcted)
54+
4455
def test_create_new_sponsorship(self):
4556
sponsorship = Sponsorship.new(
4657
self.sponsor, self.benefits, submited_by=self.user

sponsors/tests/test_notifications.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class AppliedSponsorshipNotificationToPSFTests(TestCase):
1414
def setUp(self):
1515
self.notification = notifications.AppliedSponsorshipNotificationToPSF()
1616
self.user = baker.make(settings.AUTH_USER_MODEL)
17-
self.sponsorship = baker.make("sponsors.Sponsorship")
17+
self.sponsorship = baker.make("sponsors.Sponsorship", sponsor__name="foo")
1818
self.subject_template = "sponsors/email/psf_new_application_subject.txt"
1919
self.content_template = "sponsors/email/psf_new_application.txt"
2020

@@ -41,7 +41,10 @@ def setUp(self):
4141
self.unverified_email = baker.make(EmailAddress, verified=False)
4242
self.sponsor_contacts = [
4343
baker.make(
44-
"sponsors.SponsorContact", email="[email protected]", primary=True
44+
"sponsors.SponsorContact",
45+
46+
primary=True,
47+
sponsor__name="foo",
4548
),
4649
baker.make("sponsors.SponsorContact", email=self.verified_email.email),
4750
baker.make("sponsors.SponsorContact", email=self.unverified_email.email),
@@ -79,7 +82,9 @@ class RejectedSponsorshipNotificationToPSFTests(TestCase):
7982
def setUp(self):
8083
self.notification = notifications.RejectedSponsorshipNotificationToPSF()
8184
self.sponsorship = baker.make(
82-
Sponsorship, status=Sponsorship.REJECTED, _fill_optional=["rejected_on"]
85+
Sponsorship,
86+
status=Sponsorship.REJECTED,
87+
_fill_optional=["rejected_on", "sponsor"],
8388
)
8489
self.subject_template = "sponsors/email/psf_rejected_sponsorship_subject.txt"
8590
self.content_template = "sponsors/email/psf_rejected_sponsorship.txt"
@@ -106,7 +111,7 @@ def setUp(self):
106111
self.sponsorship = baker.make(
107112
Sponsorship,
108113
status=Sponsorship.REJECTED,
109-
_fill_optional=["rejected_on"],
114+
_fill_optional=["rejected_on", "sponsor"],
110115
submited_by=self.user,
111116
)
112117
self.subject_template = (
@@ -133,7 +138,9 @@ class StatementOfWorkNotificationToPSFTests(TestCase):
133138
def setUp(self):
134139
self.notification = notifications.StatementOfWorkNotificationToPSF()
135140
self.sponsorship = baker.make(
136-
Sponsorship, status=Sponsorship.APPROVED, _fill_optional=["approved_on"]
141+
Sponsorship,
142+
status=Sponsorship.APPROVED,
143+
_fill_optional=["approved_on", "sponsor"],
137144
)
138145
self.subject_template = "sponsors/email/psf_statement_of_work_subject.txt"
139146
self.content_template = "sponsors/email/psf_statement_of_work.txt"
@@ -160,7 +167,7 @@ def setUp(self):
160167
self.sponsorship = baker.make(
161168
Sponsorship,
162169
status=Sponsorship.APPROVED,
163-
_fill_optional=["approved_on"],
170+
_fill_optional=["approved_on", "sponsor"],
164171
submited_by=self.user,
165172
)
166173
self.subject_template = "sponsors/email/sponsor_statement_of_work_subject.txt"

sponsors/tests/test_templatetags.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,28 @@
99

1010
class FullSponsorshipTemplatetagTests(TestCase):
1111
def test_templatetag_context(self):
12-
sponsorship = baker.make("sponsors.Sponsorship", _fill_optional=True)
12+
sponsorship = baker.make(
13+
"sponsors.Sponsorship", for_modified_package=False, _fill_optional=True
14+
)
1315
context = full_sponsorship(sponsorship)
1416
expected = {
1517
"sponsorship": sponsorship,
1618
"sponsor": sponsorship.sponsor,
1719
"benefits": list(sponsorship.benefits.all()),
20+
"display_fee": True,
1821
}
1922
self.assertEqual(context, expected)
23+
24+
def test_do_not_display_fee_if_modified_package(self):
25+
sponsorship = baker.make(
26+
"sponsors.Sponsorship", for_modified_package=True, _fill_optional=True
27+
)
28+
context = full_sponsorship(sponsorship)
29+
self.assertFalse(context["display_fee"])
30+
31+
def test_allows_to_overwrite_display_fee_flag(self):
32+
sponsorship = baker.make(
33+
"sponsors.Sponsorship", for_modified_package=True, _fill_optional=True
34+
)
35+
context = full_sponsorship(sponsorship, display_fee=True)
36+
self.assertTrue(context["display_fee"])

sponsors/tests/test_views.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,12 @@ def setUp(self):
327327
settings.AUTH_USER_MODEL, is_staff=True, is_superuser=True
328328
)
329329
self.client.force_login(self.user)
330-
self.sponsorship = baker.make(Sponsorship, submited_by=self.user)
330+
self.sponsorship = baker.make(
331+
Sponsorship,
332+
status=Sponsorship.APPLIED,
333+
submited_by=self.user,
334+
_fill_optional=True,
335+
)
331336
self.url = reverse(
332337
"admin:sponsors_sponsorship_reject", args=[self.sponsorship.pk]
333338
)
@@ -395,14 +400,31 @@ def test_staff_required(self):
395400

396401
self.assertRedirects(r, redirect_url, fetch_redirect_response=False)
397402

403+
def test_message_user_if_rejecting_invalid_sponsorship(self):
404+
self.sponsorship.status = Sponsorship.FINALIZED
405+
self.sponsorship.save()
406+
data = {"confirm": "yes"}
407+
response = self.client.post(self.url, data=data)
408+
self.sponsorship.refresh_from_db()
409+
410+
expected_url = reverse(
411+
"admin:sponsors_sponsorship_change", args=[self.sponsorship.pk]
412+
)
413+
self.assertRedirects(response, expected_url, fetch_redirect_response=True)
414+
self.assertEqual(self.sponsorship.status, Sponsorship.FINALIZED)
415+
msg = list(get_messages(response.wsgi_request))[0]
416+
assertMessage(msg, "Can't reject a Finalized sponsorship.", messages.ERROR)
417+
398418

399419
class ApproveSponsorshipAdminViewTests(TestCase):
400420
def setUp(self):
401421
self.user = baker.make(
402422
settings.AUTH_USER_MODEL, is_staff=True, is_superuser=True
403423
)
404424
self.client.force_login(self.user)
405-
self.sponsorship = baker.make(Sponsorship)
425+
self.sponsorship = baker.make(
426+
Sponsorship, status=Sponsorship.APPLIED, _fill_optional=True
427+
)
406428
self.url = reverse(
407429
"admin:sponsors_sponsorship_approve", args=[self.sponsorship.pk]
408430
)
@@ -468,3 +490,18 @@ def test_staff_required(self):
468490
r = self.client.get(self.url)
469491

470492
self.assertRedirects(r, redirect_url, fetch_redirect_response=False)
493+
494+
def test_message_user_if_approving_invalid_sponsorship(self):
495+
self.sponsorship.status = Sponsorship.FINALIZED
496+
self.sponsorship.save()
497+
data = {"confirm": "yes"}
498+
response = self.client.post(self.url, data=data)
499+
self.sponsorship.refresh_from_db()
500+
501+
expected_url = reverse(
502+
"admin:sponsors_sponsorship_change", args=[self.sponsorship.pk]
503+
)
504+
self.assertRedirects(response, expected_url, fetch_redirect_response=True)
505+
self.assertEqual(self.sponsorship.status, Sponsorship.FINALIZED)
506+
msg = list(get_messages(response.wsgi_request))[0]
507+
assertMessage(msg, "Can't approve a Finalized sponsorship.", messages.ERROR)

0 commit comments

Comments
 (0)