From 4ef43887cf19fd394ae7dd2ec5d1c0cf5d4d7f84 Mon Sep 17 00:00:00 2001 From: Jessie <70440141+jessiebelle@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:20:39 +0200 Subject: [PATCH] Sponsorship - adding renewal option for contract generation (#2344) * WIP renewal work * test fix * removing venv added files * tidy and fixup * test fixup after logic changes * Sponsorship renewal review (#2345) * add rewnewal to the admin view for sponsorship * use the sponsorship form directly rather than editing template * include previous effective date in context/review form for renewals * update to real renewal contract * missing migration --------- Co-authored-by: Ee Durbin --- sponsors/admin.py | 1 + sponsors/forms.py | 9 ++++- .../migrations/0097_sponsorship_renewal.py | 18 +++++++++ .../migrations/0098_auto_20231219_1910.py | 18 +++++++++ sponsors/models/sponsorship.py | 13 +++++- sponsors/pdf.py | 10 ++++- sponsors/tests/test_pdf.py | 38 ++++++++++++++++++ sponsors/tests/test_use_cases.py | 18 +++++++++ sponsors/use_cases.py | 3 ++ sponsors/views_admin.py | 6 ++- .../sponsors/admin/approve_application.html | 22 +++++++++- .../admin/renewal-contract-template.docx | Bin 0 -> 9245 bytes 12 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 sponsors/migrations/0097_sponsorship_renewal.py create mode 100644 sponsors/migrations/0098_auto_20231219_1910.py create mode 100644 templates/sponsors/admin/renewal-contract-template.docx diff --git a/sponsors/admin.py b/sponsors/admin.py index ac05a00b8..d6601140f 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -402,6 +402,7 @@ class SponsorshipAdmin(ImportExportActionModelAdmin, admin.ModelAdmin): "end_date", "get_contract", "level_name", + "renewal", "overlapped_by", ), }, diff --git a/sponsors/forms.py b/sponsors/forms.py index f4c72726a..8d262b337 100644 --- a/sponsors/forms.py +++ b/sponsors/forms.py @@ -392,6 +392,10 @@ class SponsorshipReviewAdminForm(forms.ModelForm): start_date = forms.DateField(widget=AdminDateWidget(), required=False) end_date = forms.DateField(widget=AdminDateWidget(), required=False) overlapped_by = forms.ModelChoiceField(queryset=Sponsorship.objects.select_related("sponsor", "package"), required=False) + renewal = forms.BooleanField( + help_text="If true, it means the sponsorship is a renewal of a previous sponsorship and will use the renewal template for contracting.", + required=False, + ) def __init__(self, *args, **kwargs): force_required = kwargs.pop("force_required", False) @@ -403,10 +407,12 @@ def __init__(self, *args, **kwargs): self.fields.pop("overlapped_by") # overlapped should never be displayed on approval for field_name in self.fields: self.fields[field_name].required = True + self.fields["renewal"].required = False + class Meta: model = Sponsorship - fields = ["start_date", "end_date", "package", "sponsorship_fee"] + fields = ["start_date", "end_date", "package", "sponsorship_fee", "renewal"] widgets = { 'year': SPONSORSHIP_YEAR_SELECT, } @@ -415,6 +421,7 @@ def clean(self): cleaned_data = super().clean() start_date = cleaned_data.get("start_date") end_date = cleaned_data.get("end_date") + renewal = cleaned_data.get("renewal") if start_date and end_date and end_date <= start_date: raise forms.ValidationError("End date must be greater than start date") diff --git a/sponsors/migrations/0097_sponsorship_renewal.py b/sponsors/migrations/0097_sponsorship_renewal.py new file mode 100644 index 000000000..fdbc347b3 --- /dev/null +++ b/sponsors/migrations/0097_sponsorship_renewal.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2023-12-18 16:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0096_auto_20231214_2108'), + ] + + operations = [ + migrations.AddField( + model_name='sponsorship', + name='renewal', + field=models.BooleanField(blank=True, null=True), + ), + ] diff --git a/sponsors/migrations/0098_auto_20231219_1910.py b/sponsors/migrations/0098_auto_20231219_1910.py new file mode 100644 index 000000000..3c466bb75 --- /dev/null +++ b/sponsors/migrations/0098_auto_20231219_1910.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2023-12-19 19:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0097_sponsorship_renewal'), + ] + + operations = [ + migrations.AlterField( + model_name='sponsorship', + name='renewal', + field=models.BooleanField(blank=True, help_text='If true, it means the sponsorship is a renewal of a previous sponsorship and will use the renewal template for contracting.', null=True), + ), + ] diff --git a/sponsors/models/sponsorship.py b/sponsors/models/sponsorship.py index 8e1d13a63..7443d4d2c 100644 --- a/sponsors/models/sponsorship.py +++ b/sponsors/models/sponsorship.py @@ -135,7 +135,7 @@ class Meta(OrderedModel.Meta): class Sponsorship(models.Model): """ - Represente a sponsorship application by a sponsor. + Represents a sponsorship application by a sponsor. It's responsible to group the set of selected benefits and link it to sponsor """ @@ -182,6 +182,11 @@ class Sponsorship(models.Model): package = models.ForeignKey(SponsorshipPackage, null=True, on_delete=models.SET_NULL) sponsorship_fee = models.PositiveIntegerField(null=True, blank=True) overlapped_by = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) + renewal = models.BooleanField( + null=True, + blank=True, + help_text="If true, it means the sponsorship is a renewal of a previous sponsorship and will use the renewal template for contracting." + ) assets = GenericRelation(GenericAsset) @@ -378,6 +383,12 @@ def next_status(self): } return states_map[self.status] + @property + def previous_effective_date(self): + if len(self.sponsor.sponsorship_set.all().order_by('-year')) > 1: + return self.sponsor.sponsorship_set.all().order_by('-year')[1].start_date + return None + class SponsorshipBenefit(OrderedModel): """ diff --git a/sponsors/pdf.py b/sponsors/pdf.py index 5188b8290..f1b80d911 100644 --- a/sponsors/pdf.py +++ b/sponsors/pdf.py @@ -32,7 +32,9 @@ def _contract_context(contract, **context): "sponsorship": contract.sponsorship, "benefits": _clean_split(contract.benefits_list.raw), "legal_clauses": _clean_split(contract.legal_clauses.raw), + "renewal": contract.sponsorship.renewal, }) + context["previous_effective"] = contract.sponsorship.previous_effective_date if contract.sponsorship.previous_effective_date else "UNKNOWN" return context @@ -49,9 +51,13 @@ def render_contract_to_pdf_file(contract, **context): def _gen_docx_contract(output, contract, **context): - template = os.path.join(settings.TEMPLATES_DIR, "sponsors", "admin", "contract-template.docx") - doc = DocxTemplate(template) context = _contract_context(contract, **context) + renewal = context["renewal"] + if renewal: + template = os.path.join(settings.TEMPLATES_DIR, "sponsors", "admin", "renewal-contract-template.docx") + else: + template = os.path.join(settings.TEMPLATES_DIR, "sponsors", "admin", "contract-template.docx") + doc = DocxTemplate(template) doc.render(context) doc.save(output) return output diff --git a/sponsors/tests/test_pdf.py b/sponsors/tests/test_pdf.py index ec929d05e..2116b7c21 100644 --- a/sponsors/tests/test_pdf.py +++ b/sponsors/tests/test_pdf.py @@ -28,6 +28,8 @@ def setUp(self): "sponsorship": self.contract.sponsorship, "benefits": [], "legal_clauses": [], + "renewal": None, + "previous_effective": "UNKNOWN", } self.template = "sponsors/admin/preview-contract.html" @@ -71,3 +73,39 @@ def test_render_response_with_docx_attachment(self, MockDocxTemplate): response.get("Content-Type"), "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ) + + @patch("sponsors.pdf.DocxTemplate") + def test_render_response_with_docx_attachment__renewal(self, MockDocxTemplate): + renewal_contract = baker.make_recipe("sponsors.tests.empty_contract", sponsorship__start_date=date.today(), + sponsorship__renewal=True) + text = f"{renewal_contract.benefits_list.raw}\n\n**Legal Clauses**\n{renewal_contract.legal_clauses.raw}" + html = render_md(text) + renewal_context = { + "contract": renewal_contract, + "start_date": renewal_contract.sponsorship.start_date, + "start_day_english_suffix": format(self.contract.sponsorship.start_date, "S"), + "sponsor": renewal_contract.sponsorship.sponsor, + "sponsorship": renewal_contract.sponsorship, + "benefits": [], + "legal_clauses": [], + "renewal": True, + "previous_effective": "UNKNOWN", + } + renewal_template = "sponsors/admin/preview-contract.html" + + template = Path(settings.TEMPLATES_DIR) / "sponsors" / "admin" / "renewal-contract-template.docx" + self.assertTrue(template.exists()) + mocked_doc = Mock(DocxTemplate) + MockDocxTemplate.return_value = mocked_doc + + request = Mock(HttpRequest) + response = render_contract_to_docx_response(request, renewal_contract) + + MockDocxTemplate.assert_called_once_with(str(template.resolve())) + mocked_doc.render.assert_called_once_with(renewal_context) + mocked_doc.save.assert_called_once_with(response) + self.assertEqual(response.get("Content-Disposition"), "attachment; filename=contract.docx") + self.assertEqual( + response.get("Content-Type"), + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) diff --git a/sponsors/tests/test_use_cases.py b/sponsors/tests/test_use_cases.py index 433d4950e..3e5e5ad04 100644 --- a/sponsors/tests/test_use_cases.py +++ b/sponsors/tests/test_use_cases.py @@ -118,6 +118,24 @@ def test_update_sponsorship_as_approved_and_create_contract(self): self.assertEqual(self.sponsorship.sponsorship_fee, 100) self.assertEqual(self.sponsorship.package, self.package) self.assertEqual(self.sponsorship.level_name, self.package.name) + self.assertFalse(self.sponsorship.renewal) + + + def test_update_renewal_sponsorship_as_approved_and_create_contract(self): + self.data.update({"renewal": True}) + self.use_case.execute(self.sponsorship, **self.data) + self.sponsorship.refresh_from_db() + + today = timezone.now().date() + self.assertEqual(self.sponsorship.approved_on, today) + self.assertEqual(self.sponsorship.status, Sponsorship.APPROVED) + self.assertTrue(self.sponsorship.contract.pk) + self.assertTrue(self.sponsorship.start_date) + self.assertTrue(self.sponsorship.end_date) + self.assertEqual(self.sponsorship.sponsorship_fee, 100) + self.assertEqual(self.sponsorship.package, self.package) + self.assertEqual(self.sponsorship.level_name, self.package.name) + self.assertEqual(self.sponsorship.renewal, True) def test_send_notifications_using_sponsorship(self): self.use_case.execute(self.sponsorship, **self.data) diff --git a/sponsors/use_cases.py b/sponsors/use_cases.py index 95b2d267e..bbb6f2483 100644 --- a/sponsors/use_cases.py +++ b/sponsors/use_cases.py @@ -55,11 +55,14 @@ def execute(self, sponsorship, start_date, end_date, **kwargs): sponsorship.approve(start_date, end_date) package = kwargs.get("package") fee = kwargs.get("sponsorship_fee") + renewal = kwargs.get("renewal", False) if package: sponsorship.package = package sponsorship.level_name = package.name if fee: sponsorship.sponsorship_fee = fee + if renewal: + sponsorship.renewal = True sponsorship.save() contract = Contract.new(sponsorship) diff --git a/sponsors/views_admin.py b/sponsors/views_admin.py index 8968da1b7..e9a808ccc 100644 --- a/sponsors/views_admin.py +++ b/sponsors/views_admin.py @@ -85,7 +85,11 @@ def approve_sponsorship_view(ModelAdmin, request, pk): ) return redirect(redirect_url) - context = {"sponsorship": sponsorship, "form": form} + context = { + "sponsorship": sponsorship, + "form": form, + "previous_effective": sponsorship.previous_effective_date if sponsorship.previous_effective_date else "UNKNOWN", + } return render(request, "sponsors/admin/approve_application.html", context=context) diff --git a/templates/sponsors/admin/approve_application.html b/templates/sponsors/admin/approve_application.html index 37ce49cdc..42a5f7382 100644 --- a/templates/sponsors/admin/approve_application.html +++ b/templates/sponsors/admin/approve_application.html @@ -2,7 +2,18 @@ {% extends 'admin/change_form.html' %} {% load i18n static sponsors %} -{% block extrastyle %}{{ block.super }}{% endblock %} +{% block extrastyle %} + {{ block.super }} + + +{% endblock %} {% block title %}Accept {{ sponsorship }} | python.org{% endblock %} @@ -33,8 +44,15 @@

Generate Contract for Signing

{{ form.media }} {{ form.as_p }} - +

+ + {{ previous_effective }} + The last known contract effective date for this sponsor. This will only impact renewals. If UNKNOWN, you MUST update the resulting docx with the correct effective date. +

+
diff --git a/templates/sponsors/admin/renewal-contract-template.docx b/templates/sponsors/admin/renewal-contract-template.docx new file mode 100644 index 0000000000000000000000000000000000000000..97b8d1cc673cb9df49be633c2b7e3e1d114f829e GIT binary patch literal 9245 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 V2m9>85D>`EkFIAoktKgQ`!AfFXAA%U literal 0 HcmV?d00001