Skip to content

Commit ceb0bf9

Browse files
authored
Do not display repeated sponsorship applications (#2003)
* Ignore envrc files used by direnv * Add new FK on sponsorship to point to most recent sponsorship for that sponsor * Explicitly set up end date on contract recipes * Refactor code by extracting query to look up for application active on an specific date * Link overlapped applications by the new one which is being executed * Overlapped sponsorship applications shouldn't be enabled to be visible * Add overlapped by field to django admin so PSF staff can configure it * Only list applications from the same sponsor if editing an application * Do not display overlapped by field when approving an sponsorship
1 parent 077a7b9 commit ceb0bf9

File tree

10 files changed

+108
-4
lines changed

10 files changed

+108
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ __pycache__
2424
.coverage
2525
.env
2626
.DS_Store
27+
.envrc

sponsors/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ class SponsorshipAdmin(admin.ModelAdmin):
295295
"end_date",
296296
"get_contract",
297297
"level_name",
298+
"overlapped_by",
298299
),
299300
},
300301
),

sponsors/forms.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,11 +375,16 @@ def user_with_previous_sponsors(self):
375375
class SponsorshipReviewAdminForm(forms.ModelForm):
376376
start_date = forms.DateField(widget=AdminDateWidget(), required=False)
377377
end_date = forms.DateField(widget=AdminDateWidget(), required=False)
378+
overlapped_by = forms.ModelChoiceField(queryset=Sponsorship.objects.select_related("sponsor", "package"), required=False)
378379

379380
def __init__(self, *args, **kwargs):
380381
force_required = kwargs.pop("force_required", False)
381382
super().__init__(*args, **kwargs)
383+
if self.instance:
384+
qs = self.fields["overlapped_by"].queryset.exclude(id=self.instance.id)
385+
self.fields["overlapped_by"].queryset = qs.filter(sponsor_id=self.instance.sponsor_id)
382386
if force_required:
387+
self.fields.pop("overlapped_by") # overlapped should never be displayed on approval
383388
for field_name in self.fields:
384389
self.fields[field_name].required = True
385390

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 2.2.24 on 2022-03-03 20:23
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django.db.models.manager
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('sponsors', '0074_auto_20220211_1659'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='sponsorship',
17+
name='overlapped_by',
18+
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='sponsors.Sponsorship'),
19+
),
20+
]

sponsors/models/managers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ def visible_to(self, user):
2525
def finalized(self):
2626
return self.filter(status=self.model.FINALIZED)
2727

28+
def active_on_date(self, ref_date):
29+
return self.filter(start_date__lte=ref_date, end_date__gte=ref_date)
30+
2831
def enabled(self):
2932
"""Sponsorship which are finalized and enabled"""
3033
today = timezone.now().date()
31-
qs = self.finalized()
32-
return qs.filter(start_date__lte=today, end_date__gte=today)
34+
return self.finalized().active_on_date(today).exclude(overlapped_by__isnull=False)
3335

3436
def with_logo_placement(self, logo_place=None, publisher=None):
3537
from sponsors.models import LogoPlacement, SponsorBenefit

sponsors/models/sponsorship.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class Sponsorship(models.Model):
151151
"name")
152152
package = models.ForeignKey(SponsorshipPackage, null=True, on_delete=models.SET_NULL)
153153
sponsorship_fee = models.PositiveIntegerField(null=True, blank=True)
154+
overlapped_by = models.ForeignKey("self", null=True, on_delete=models.SET_NULL)
154155

155156
assets = GenericRelation(GenericAsset)
156157

sponsors/tests/baker_recipes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
today = date.today()
99
two_days = timedelta(days=2)
10+
thirty_days = timedelta(days=30)
1011

1112
empty_contract = Recipe(
1213
Contract,
1314
sponsorship__sponsor__name="Sponsor",
1415
sponsorship__start_date=today,
16+
sponsorship__end_date=today + thirty_days,
1517
benefits_list="",
1618
legal_clauses="",
1719
)
@@ -20,6 +22,7 @@
2022
Contract,
2123
sponsorship__sponsor__name="Awaiting Sponsor",
2224
sponsorship__start_date=today,
25+
sponsorship__end_date=today + thirty_days,
2326
benefits_list="- benefit 1",
2427
legal_clauses="",
2528
status=Contract.AWAITING_SIGNATURE,

sponsors/tests/test_managers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ def test_enabled_sponsorships(self):
6363
start_date=today - 2 * two_days,
6464
end_date=today - two_days
6565
)
66+
# shouldn't list overlapped sponsorships
67+
baker.make(
68+
Sponsorship,
69+
status=Sponsorship.FINALIZED,
70+
start_date=today - two_days,
71+
end_date=today + two_days,
72+
overlapped_by=enabled,
73+
)
6674

6775
qs = Sponsorship.objects.enabled()
6876

sponsors/tests/test_use_cases.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from sponsors import use_cases
1414
from sponsors.notifications import *
15-
from sponsors.models import Sponsorship, Contract, SponsorEmailNotificationTemplate
15+
from sponsors.models import Sponsorship, Contract, SponsorEmailNotificationTemplate, Sponsor
1616

1717

1818
class CreateSponsorshipApplicationUseCaseTests(TestCase):
@@ -220,7 +220,7 @@ def test_execute_and_update_database_object(self):
220220
self.assertEqual(b"Contract content", self.contract.signed_document.read())
221221
self.assertEqual(f"{Contract.SIGNED_PDF_DIR}1234.txt", self.contract.signed_document.name)
222222

223-
def test_build_use_case_with_default_notificationss(self):
223+
def test_build_use_case_with_default_notifications(self):
224224
uc = use_cases.ExecuteExistingContractUseCase.build()
225225
self.assertEqual(len(uc.notifications), 2)
226226
self.assertIsInstance(
@@ -230,6 +230,63 @@ def test_build_use_case_with_default_notificationss(self):
230230
uc.notifications[1], RefreshSponsorshipsCache,
231231
)
232232

233+
def test_execute_contract_flag_overlapping_sponsorships(self):
234+
sponsorship = self.contract.sponsorship
235+
self.use_case.execute(self.contract, self.file)
236+
self.contract.refresh_from_db()
237+
recent_contract = baker.make_recipe(
238+
"sponsors.tests.empty_contract",
239+
status=Contract.DRAFT,
240+
sponsorship__sponsor=sponsorship.sponsor,
241+
sponsorship__start_date=sponsorship.start_date + timedelta(days=5),
242+
sponsorship__end_date=sponsorship.end_date + timedelta(days=5),
243+
)
244+
245+
self.use_case.execute(recent_contract, self.file)
246+
recent_contract.refresh_from_db()
247+
sponsorship.refresh_from_db()
248+
249+
self.assertEqual(recent_contract.status, Contract.EXECUTED)
250+
self.assertEqual(sponsorship.overlapped_by, recent_contract.sponsorship)
251+
252+
def test_execute_contract_do_not_flag_overlap_if_no_date_range_conflict(self):
253+
sponsorship = self.contract.sponsorship
254+
self.use_case.execute(self.contract, self.file)
255+
self.contract.refresh_from_db()
256+
recent_contract = baker.make_recipe(
257+
"sponsors.tests.empty_contract",
258+
status=Contract.DRAFT,
259+
sponsorship__sponsor=sponsorship.sponsor,
260+
sponsorship__start_date=sponsorship.end_date + timedelta(days=1),
261+
sponsorship__end_date=sponsorship.end_date + timedelta(days=5),
262+
)
263+
264+
self.use_case.execute(recent_contract, self.file)
265+
recent_contract.refresh_from_db()
266+
sponsorship.refresh_from_db()
267+
268+
self.assertEqual(recent_contract.status, Contract.EXECUTED)
269+
self.assertIsNone(sponsorship.overlapped_by)
270+
271+
def test_execute_contract_do_not_flag_overlap_if_from_other_sponsor(self):
272+
sponsorship = self.contract.sponsorship
273+
self.use_case.execute(self.contract, self.file)
274+
self.contract.refresh_from_db()
275+
recent_contract = baker.make_recipe(
276+
"sponsors.tests.empty_contract",
277+
status=Contract.DRAFT,
278+
sponsorship__sponsor=baker.make(Sponsor),
279+
sponsorship__start_date=sponsorship.start_date + timedelta(days=5),
280+
sponsorship__end_date=sponsorship.end_date + timedelta(days=5),
281+
)
282+
283+
self.use_case.execute(recent_contract, self.file)
284+
recent_contract.refresh_from_db()
285+
sponsorship.refresh_from_db()
286+
287+
self.assertEqual(recent_contract.status, Contract.EXECUTED)
288+
self.assertIsNone(sponsorship.overlapped_by)
289+
233290

234291
class NullifyContractUseCaseTests(TestCase):
235292
def setUp(self):

sponsors/use_cases.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ class ExecuteExistingContractUseCase(BaseUseCaseWithNotifications):
101101
def execute(self, contract, contract_file, **kwargs):
102102
contract.signed_document = contract_file
103103
contract.execute(force=self.force_execute)
104+
overlapping_sponsorship = Sponsorship.objects.filter(
105+
sponsor=contract.sponsorship.sponsor,
106+
).exclude(
107+
id=contract.sponsorship.id
108+
).enabled().active_on_date(contract.sponsorship.start_date)
109+
overlapping_sponsorship.update(overlapped_by=contract.sponsorship)
104110
self.notify(
105111
request=kwargs.get("request"),
106112
contract=contract,

0 commit comments

Comments
 (0)