From f59b9289619a1c1086cfe597b97ad2a981be49f4 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 13 Jan 2022 15:21:24 -0300 Subject: [PATCH 01/13] Make sure delete operations work as expected for Polymorphic models While testing the PR, I discovered this bug because I couldn't delete applications I've created. After doing some research, I figured out this is due to a known bug on django-polymorphic. More on this issue can be found in this issue: https://github.com/django-polymorphic/django-polymorphic/issues/229 --- sponsors/models/sponsors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sponsors/models/sponsors.py b/sponsors/models/sponsors.py index 7475281a7..fad84d7dc 100644 --- a/sponsors/models/sponsors.py +++ b/sponsors/models/sponsors.py @@ -273,7 +273,6 @@ def reset_attributes(self, benefit): self.save() - def delete(self): self.features.all().delete() super().delete() From fba8423b71097ac9a1307a0735d83ebff1514c37 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 13 Jan 2022 16:32:19 -0300 Subject: [PATCH 02/13] Fix identation --- sponsors/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sponsors/admin.py b/sponsors/admin.py index 243d20d7f..aba5a2081 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -48,8 +48,8 @@ class SponsorshipProgramAdmin(OrderedModelAdmin): class MultiPartForceForm(ModelForm): - def is_multipart(self): - return True + def is_multipart(self): + return True class BenefitFeatureConfigurationInline(StackedPolymorphicInline): From 452b95a45e455186ab22789202fc19ca46041300 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 13 Jan 2022 16:32:40 -0300 Subject: [PATCH 03/13] Add new due date column to required assets --- .../migrations/0071_auto_20220113_1843.py | 33 +++++++++++++++++++ sponsors/models/benefits.py | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 sponsors/migrations/0071_auto_20220113_1843.py diff --git a/sponsors/migrations/0071_auto_20220113_1843.py b/sponsors/migrations/0071_auto_20220113_1843.py new file mode 100644 index 000000000..7c66e5ba5 --- /dev/null +++ b/sponsors/migrations/0071_auto_20220113_1843.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.24 on 2022-01-13 18:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0070_auto_20220111_2055'), + ] + + operations = [ + migrations.AddField( + model_name='requiredimgasset', + name='due_date', + field=models.DateField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='requiredimgassetconfiguration', + name='due_date', + field=models.DateField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='requiredtextasset', + name='due_date', + field=models.DateField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name='requiredtextassetconfiguration', + name='due_date', + field=models.DateField(blank=True, default=None, null=True), + ), + ] diff --git a/sponsors/models/benefits.py b/sponsors/models/benefits.py index c99b1e317..fb5ec6ec6 100644 --- a/sponsors/models/benefits.py +++ b/sponsors/models/benefits.py @@ -88,6 +88,8 @@ class Meta: class BaseRequiredAsset(BaseAsset): + due_date = models.DateField(default=None, null=True, blank=True) + class Meta: abstract = True From 38bd60be0ab853f23788147fb336d460cd4dd6df Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Thu, 13 Jan 2022 16:33:20 -0300 Subject: [PATCH 04/13] Base command to notify sponsorship applications which have expiring required assets --- .../check_sponsorship_assets_due_date.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 sponsors/management/commands/check_sponsorship_assets_due_date.py diff --git a/sponsors/management/commands/check_sponsorship_assets_due_date.py b/sponsors/management/commands/check_sponsorship_assets_due_date.py new file mode 100644 index 000000000..03d61543b --- /dev/null +++ b/sponsors/management/commands/check_sponsorship_assets_due_date.py @@ -0,0 +1,56 @@ +from datetime import timedelta + +from django.core.management import BaseCommand +from django.db.models import Subquery +from django.utils import timezone + +from sponsors.models import Sponsorship, Contract, BenefitFeature + + +class Command(BaseCommand): + """ + This command will query for the sponsorships which have any required asset + with a due date expiring within the certain amount of days + """ + help = "Send notifications to sponsorship with pending required assets" + + def add_arguments(self, parser): + help = "Num of days to be used as interval up to target date" + parser.add_argument("num_days", nargs="?", default="7", help=help) + parser.add_argument("--no-input", action="store_true", help="Tells Django to NOT prompt the user for input of any kind.") + + def handle(self, **options): + num_days = options["num_days"] + ask_input = not options["no_input"] + target_date = timezone.now() - timedelta(days=int(num_days)) + + req_assets = BenefitFeature.objects.required_assets() + + sponsorship_ids = Subquery(req_assets.values_list("sponsor_benefit__sponsorship_id").distinct()) + sponsorships = Sponsorship.objects.filter(id__in=sponsorship_ids) + + sponsorships_to_notify = [] + for sponsorship in sponsorships: + to_notify = any([ + asset.due_date == target_date + for asset in req_assets.from_sponsorship(sponsorship) + if asset.due_date + ]) + if to_notify: + sponsorships_to_notify.append(sponsorship) + + if sponsorships_to_notify: + print("No sponsorship with required assets with due date close to expiration.") + return + + user_input = "" + while user_input != "Y" and ask_input: + msg = f"Contacts from {len(sponsorships_to_notify)} with pending assets with expiring due date will get " \ + f"notified. " + msg += "Do you want to proceed? [Y/n]: " + user_input = input(msg).strip().upper() + if user_input == "N": + print("Finishing execution.") + return + elif user_input != "Y": + print("Invalid option...") From fb5d8080da82379e212bc021ff716869f4531e6e Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 21 Jan 2022 15:58:52 -0300 Subject: [PATCH 05/13] Create new notification about required assets close to due date --- sponsors/notifications.py | 14 +++++ sponsors/tests/test_notifications.py | 56 +++++++++++++++++++ .../email/sponsor_expiring_assets.txt | 10 ++++ .../email/sponsor_expiring_assets_subject.txt | 1 + 4 files changed, 81 insertions(+) create mode 100644 templates/sponsors/email/sponsor_expiring_assets.txt create mode 100644 templates/sponsors/email/sponsor_expiring_assets_subject.txt diff --git a/sponsors/notifications.py b/sponsors/notifications.py index 66c980550..e2cccfff4 100644 --- a/sponsors/notifications.py +++ b/sponsors/notifications.py @@ -216,3 +216,17 @@ class RefreshSponsorshipsCache: def notify(self, *args, **kwargs): # clean up cached used by "sponsors/partials/sponsors-list.html" cache.delete("CACHED_SPONSORS_LIST") + + +class AssetCloseToDueDateNotificationToSponsors(BaseEmailSponsorshipNotification): + subject_template = "sponsors/email/sponsor_expiring_assets_subject.txt" + message_template = "sponsors/email/sponsor_expiring_assets.txt" + email_context_keys = ["sponsorship", "required_assets", "due_date"] + + def get_recipient_list(self, context): + return context["sponsorship"].verified_emails + + def get_email_context(self, **kwargs): + context = super().get_email_context(**kwargs) + context["required_assets"] = BenefitFeature.objects.from_sponsorship(context["sponsorship"]).required_assets() + return context diff --git a/sponsors/tests/test_notifications.py b/sponsors/tests/test_notifications.py index 27edc2501..47f022a8c 100644 --- a/sponsors/tests/test_notifications.py +++ b/sponsors/tests/test_notifications.py @@ -428,3 +428,59 @@ def test_create_log_entry(self): self.assertEqual(str(self.sponsorship), log_entry.object_repr) self.assertEqual(log_entry.action_flag, CHANGE) self.assertEqual(log_entry.change_message, "Notification 'Foo' was sent to contacts: administrative") + + +class AssetCloseToDueDateNotificationToSponsorsTestCase(TestCase): + def setUp(self): + self.notification = notifications.AssetCloseToDueDateNotificationToSponsors() + self.user = baker.make(settings.AUTH_USER_MODEL, email="foo@foo.com") + self.verified_email = baker.make(EmailAddress, verified=True) + self.unverified_email = baker.make(EmailAddress, verified=False) + self.sponsor_contacts = [ + baker.make( + "sponsors.SponsorContact", + email="foo@example.com", + primary=True, + sponsor__name="foo", + ), + baker.make("sponsors.SponsorContact", email=self.verified_email.email), + baker.make("sponsors.SponsorContact", email=self.unverified_email.email), + ] + self.sponsor = baker.make("sponsors.Sponsor", contacts=self.sponsor_contacts) + self.sponsorship = baker.make( + "sponsors.Sponsorship", sponsor=self.sponsor, submited_by=self.user + ) + self.subject_template = "sponsors/email/sponsor_expiring_assets_subject.txt" + self.content_template = "sponsors/email/sponsor_expiring_assets.txt" + + def test_send_email_using_correct_templates(self): + context = {"sponsorship": self.sponsorship} + expected_subject = render_to_string(self.subject_template, context).strip() + expected_content = render_to_string(self.content_template, context).strip() + + self.notification.notify(sponsorship=self.sponsorship) + self.assertTrue(mail.outbox) + + email = mail.outbox[0] + self.assertEqual(expected_subject, email.subject) + self.assertEqual(expected_content, email.body) + self.assertEqual(settings.SPONSORSHIP_NOTIFICATION_FROM_EMAIL, email.from_email) + self.assertCountEqual([self.user.email, self.verified_email.email], email.to) + + def test_send_email_to_correct_recipients(self): + context = {"user": self.user, "sponsorship": self.sponsorship} + expected_contacts = ["foo@foo.com", self.verified_email.email] + self.assertCountEqual( + expected_contacts, self.notification.get_recipient_list(context) + ) + + def test_list_required_assets_in_email_context(self): + cfg = baker.make(RequiredTextAssetConfiguration, internal_name='input') + benefit = baker.make(SponsorBenefit, sponsorship=self.sponsorship) + asset = cfg.create_benefit_feature(benefit) + base_context = {"sponsorship": self.sponsorship} + context = self.notification.get_email_context(**base_context) + self.assertEqual(2, len(context)) + self.assertEqual(self.sponsorship, context["sponsorship"]) + self.assertEqual(1, len(context["required_assets"])) + self.assertIn(asset, context["required_assets"]) diff --git a/templates/sponsors/email/sponsor_expiring_assets.txt b/templates/sponsors/email/sponsor_expiring_assets.txt new file mode 100644 index 000000000..5607baf33 --- /dev/null +++ b/templates/sponsors/email/sponsor_expiring_assets.txt @@ -0,0 +1,10 @@ +Hi, + +Your sponsorship application for {{ sponsorship.sponsor.name }} requires one or more assets with {{ due_date|date }} as due date. + +Please, submit all information in ordeer for PSF to proceed with your application. You can submit your assets via the following link: + +https://python.org{% url "users:sponsorship_application_detail" sponsorship.pk %} + +Thanks, +Python Software Foundation diff --git a/templates/sponsors/email/sponsor_expiring_assets_subject.txt b/templates/sponsors/email/sponsor_expiring_assets_subject.txt new file mode 100644 index 000000000..32ea06fd5 --- /dev/null +++ b/templates/sponsors/email/sponsor_expiring_assets_subject.txt @@ -0,0 +1 @@ +You have pendings assets for your application From 4bef41e796479b53e36ed67257138deb43225d6a Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 21 Jan 2022 15:59:36 -0300 Subject: [PATCH 06/13] Dispatch notifications via management command --- .../commands/check_sponsorship_assets_due_date.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sponsors/management/commands/check_sponsorship_assets_due_date.py b/sponsors/management/commands/check_sponsorship_assets_due_date.py index 03d61543b..13a631faf 100644 --- a/sponsors/management/commands/check_sponsorship_assets_due_date.py +++ b/sponsors/management/commands/check_sponsorship_assets_due_date.py @@ -5,6 +5,7 @@ from django.utils import timezone from sponsors.models import Sponsorship, Contract, BenefitFeature +from sponsors.notifications import AssetCloseToDueDateNotificationToSponsors class Command(BaseCommand): @@ -39,7 +40,7 @@ def handle(self, **options): if to_notify: sponsorships_to_notify.append(sponsorship) - if sponsorships_to_notify: + if not sponsorships_to_notify: print("No sponsorship with required assets with due date close to expiration.") return @@ -54,3 +55,10 @@ def handle(self, **options): return elif user_input != "Y": print("Invalid option...") + + notification = AssetCloseToDueDateNotificationToSponsors() + for sponsorship in sponsorships_to_notify: + kwargs = {"sponsorship": sponsorship, "due_date": target_date} + notification.notify(**kwargs) + + print("Notifications sent!") From 93ab969d49271d0a4c0940376b6d8a0e41ddc422 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 21 Jan 2022 16:00:56 -0300 Subject: [PATCH 07/13] Management command should look for future expiration dates --- .../management/commands/check_sponsorship_assets_due_date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sponsors/management/commands/check_sponsorship_assets_due_date.py b/sponsors/management/commands/check_sponsorship_assets_due_date.py index 13a631faf..edc8ddcff 100644 --- a/sponsors/management/commands/check_sponsorship_assets_due_date.py +++ b/sponsors/management/commands/check_sponsorship_assets_due_date.py @@ -23,7 +23,7 @@ def add_arguments(self, parser): def handle(self, **options): num_days = options["num_days"] ask_input = not options["no_input"] - target_date = timezone.now() - timedelta(days=int(num_days)) + target_date = timezone.now() + timedelta(days=int(num_days)) req_assets = BenefitFeature.objects.required_assets() From d8488f4b9f717ad04e5eaa3038c717f8ad673e7b Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 21 Jan 2022 16:14:35 -0300 Subject: [PATCH 08/13] Add test to ensure due date within the email context --- sponsors/tests/test_notifications.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sponsors/tests/test_notifications.py b/sponsors/tests/test_notifications.py index 47f022a8c..b3813eddf 100644 --- a/sponsors/tests/test_notifications.py +++ b/sponsors/tests/test_notifications.py @@ -1,3 +1,4 @@ +from datetime import date from unittest.mock import Mock from model_bakery import baker @@ -478,9 +479,10 @@ def test_list_required_assets_in_email_context(self): cfg = baker.make(RequiredTextAssetConfiguration, internal_name='input') benefit = baker.make(SponsorBenefit, sponsorship=self.sponsorship) asset = cfg.create_benefit_feature(benefit) - base_context = {"sponsorship": self.sponsorship} + base_context = {"sponsorship": self.sponsorship, "due_date": date.today()} context = self.notification.get_email_context(**base_context) - self.assertEqual(2, len(context)) + self.assertEqual(3, len(context)) self.assertEqual(self.sponsorship, context["sponsorship"]) self.assertEqual(1, len(context["required_assets"])) + self.assertEqual(date.today(), context["due_date"]) self.assertIn(asset, context["required_assets"]) From 1b78898f558eb6f1cb678e987baad7707ac58a83 Mon Sep 17 00:00:00 2001 From: Bernardo Fontes Date: Fri, 21 Jan 2022 16:14:57 -0300 Subject: [PATCH 09/13] Disable asset input if past due date --- sponsors/forms.py | 7 ++++++- sponsors/tests/test_forms.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/sponsors/forms.py b/sponsors/forms.py index ee845a0a7..d467b570e 100644 --- a/sponsors/forms.py +++ b/sponsors/forms.py @@ -1,3 +1,4 @@ +from datetime import date from itertools import chain from django import forms from django.conf import settings @@ -613,12 +614,16 @@ def __init__(self, *args, **kwargs): if required_assets_ids: self.required_assets = self.required_assets.filter(pk__in=required_assets_ids) + today = date.today() fields = {} for required_asset in self.required_assets: value = required_asset.value f_name = self._get_field_name(required_asset) required = bool(value) - fields[f_name] = required_asset.as_form_field(required=required, initial=value) + field = required_asset.as_form_field(required=required, initial=value) + if required_asset.due_date and required_asset.due_date < today: + field.widget.attrs["readonly"] = True + fields[f_name] = field self.fields.update(fields) diff --git a/sponsors/tests/test_forms.py b/sponsors/tests/test_forms.py index 8e960e71d..74177f195 100644 --- a/sponsors/tests/test_forms.py +++ b/sponsors/tests/test_forms.py @@ -1,7 +1,9 @@ +from datetime import timedelta from model_bakery import baker from django.conf import settings from django.test import TestCase +from django.utils import timezone from sponsors.forms import ( SponsorshipsBenefitsForm, @@ -662,6 +664,7 @@ def setUp(self): RequiredTextAssetConfiguration, related_to=AssetsRelatedTo.SPONSORSHIP.value, internal_name="Text Input", + due_date=timezone.now().date, _fill_optional=True, ) self.required_img_cfg = baker.make( @@ -740,6 +743,17 @@ def test_load_initial_from_assets_and_force_field_if_previous_Data(self): def test_raise_error_if_form_initialized_without_instance(self): self.assertRaises(TypeError, SponsorRequiredAssetsForm) + def test_disable_input_if_wrong_past_due_date(self): + self.required_text_cfg.due_date -= timedelta(days=1) # yesterday + self.required_text_cfg.save() + text_asset = self.required_text_cfg.create_benefit_feature(self.benefits[0]) + + form = SponsorRequiredAssetsForm(instance=self.sponsorship) + field = dict(form.fields)["text_input"] + + self.assertTrue(field.widget.attrs["readonly"]) + self.assertTrue(form.has_input) + class SponsorshipBenefitAdminFormTests(TestCase): From 89644fef1e5755917381e01d745f15948ed026c5 Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Mon, 24 Jan 2022 16:54:18 -0500 Subject: [PATCH 10/13] Revert "Disable asset input if past due date" It's OK to allow them to submit after the deadline, as it is mostly for notifying them. If uploads after the deadline are acceptable, the sponsorship team will collect anything put into the boxes --- sponsors/forms.py | 7 +------ sponsors/tests/test_forms.py | 14 -------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/sponsors/forms.py b/sponsors/forms.py index d467b570e..ee845a0a7 100644 --- a/sponsors/forms.py +++ b/sponsors/forms.py @@ -1,4 +1,3 @@ -from datetime import date from itertools import chain from django import forms from django.conf import settings @@ -614,16 +613,12 @@ def __init__(self, *args, **kwargs): if required_assets_ids: self.required_assets = self.required_assets.filter(pk__in=required_assets_ids) - today = date.today() fields = {} for required_asset in self.required_assets: value = required_asset.value f_name = self._get_field_name(required_asset) required = bool(value) - field = required_asset.as_form_field(required=required, initial=value) - if required_asset.due_date and required_asset.due_date < today: - field.widget.attrs["readonly"] = True - fields[f_name] = field + fields[f_name] = required_asset.as_form_field(required=required, initial=value) self.fields.update(fields) diff --git a/sponsors/tests/test_forms.py b/sponsors/tests/test_forms.py index 74177f195..8e960e71d 100644 --- a/sponsors/tests/test_forms.py +++ b/sponsors/tests/test_forms.py @@ -1,9 +1,7 @@ -from datetime import timedelta from model_bakery import baker from django.conf import settings from django.test import TestCase -from django.utils import timezone from sponsors.forms import ( SponsorshipsBenefitsForm, @@ -664,7 +662,6 @@ def setUp(self): RequiredTextAssetConfiguration, related_to=AssetsRelatedTo.SPONSORSHIP.value, internal_name="Text Input", - due_date=timezone.now().date, _fill_optional=True, ) self.required_img_cfg = baker.make( @@ -743,17 +740,6 @@ def test_load_initial_from_assets_and_force_field_if_previous_Data(self): def test_raise_error_if_form_initialized_without_instance(self): self.assertRaises(TypeError, SponsorRequiredAssetsForm) - def test_disable_input_if_wrong_past_due_date(self): - self.required_text_cfg.due_date -= timedelta(days=1) # yesterday - self.required_text_cfg.save() - text_asset = self.required_text_cfg.create_benefit_feature(self.benefits[0]) - - form = SponsorRequiredAssetsForm(instance=self.sponsorship) - field = dict(form.fields)["text_input"] - - self.assertTrue(field.widget.attrs["readonly"]) - self.assertTrue(form.has_input) - class SponsorshipBenefitAdminFormTests(TestCase): From 8c7c6dc6567f9c65d0fa0e74edd49c97334b4822 Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Mon, 24 Jan 2022 17:23:09 -0500 Subject: [PATCH 11/13] cast "target_date" to a datetime.date so comparison works --- .../management/commands/check_sponsorship_assets_due_date.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sponsors/management/commands/check_sponsorship_assets_due_date.py b/sponsors/management/commands/check_sponsorship_assets_due_date.py index edc8ddcff..d81f212f8 100644 --- a/sponsors/management/commands/check_sponsorship_assets_due_date.py +++ b/sponsors/management/commands/check_sponsorship_assets_due_date.py @@ -23,7 +23,7 @@ def add_arguments(self, parser): def handle(self, **options): num_days = options["num_days"] ask_input = not options["no_input"] - target_date = timezone.now() + timedelta(days=int(num_days)) + target_date = (timezone.now() + timedelta(days=int(num_days))).date() req_assets = BenefitFeature.objects.required_assets() From b8dcc91d26caf203c21eeafcfa0c153b6c43aa67 Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Tue, 25 Jan 2022 14:52:23 -0500 Subject: [PATCH 12/13] update styling/wording on forms and emails --- sponsors/forms.py | 19 +++++++++++++++++-- .../email/sponsor_expiring_assets.txt | 6 +++--- .../email/sponsor_expiring_assets_subject.txt | 2 +- templates/users/sponsorship_detail.html | 6 +++--- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/sponsors/forms.py b/sponsors/forms.py index ee845a0a7..e436fa9a3 100644 --- a/sponsors/forms.py +++ b/sponsors/forms.py @@ -1,3 +1,4 @@ +import datetime from itertools import chain from django import forms from django.conf import settings @@ -5,6 +6,7 @@ from django.db.models import Q from django.utils import timezone from django.utils.functional import cached_property +from django.utils.safestring import mark_safe from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ from django_countries.fields import CountryField @@ -614,11 +616,24 @@ def __init__(self, *args, **kwargs): self.required_assets = self.required_assets.filter(pk__in=required_assets_ids) fields = {} - for required_asset in self.required_assets: + ordered_assets = sorted( + self.required_assets, + key=lambda x: (-int(bool(x.value)), x.due_date if x.due_date else datetime.date.min), + reverse=True, + ) + + for required_asset in ordered_assets: value = required_asset.value f_name = self._get_field_name(required_asset) required = bool(value) - fields[f_name] = required_asset.as_form_field(required=required, initial=value) + field = required_asset.as_form_field(required=required, initial=value) + + if required_asset.due_date and not bool(value): + field.label = mark_safe(f"{field.label} (Required by {required_asset.due_date})") + if bool(value): + field.label = mark_safe(f"{field.label} (Fulfilled, thank you!)") + + fields[f_name] = field self.fields.update(fields) diff --git a/templates/sponsors/email/sponsor_expiring_assets.txt b/templates/sponsors/email/sponsor_expiring_assets.txt index 5607baf33..4c86d903c 100644 --- a/templates/sponsors/email/sponsor_expiring_assets.txt +++ b/templates/sponsors/email/sponsor_expiring_assets.txt @@ -1,8 +1,8 @@ -Hi, +Hello {{ sponsorship.sponsor.name }} team!, -Your sponsorship application for {{ sponsorship.sponsor.name }} requires one or more assets with {{ due_date|date }} as due date. +Your sponsorship requires one or more assets by {{ due_date|date }} in order for us to fulfill your benefits: -Please, submit all information in ordeer for PSF to proceed with your application. You can submit your assets via the following link: +You can submit your assets via the following link: https://python.org{% url "users:sponsorship_application_detail" sponsorship.pk %} diff --git a/templates/sponsors/email/sponsor_expiring_assets_subject.txt b/templates/sponsors/email/sponsor_expiring_assets_subject.txt index 32ea06fd5..82d3a2c86 100644 --- a/templates/sponsors/email/sponsor_expiring_assets_subject.txt +++ b/templates/sponsors/email/sponsor_expiring_assets_subject.txt @@ -1 +1 @@ -You have pendings assets for your application +The PSF needs more information in order fulill some of your Sponsorhip benefits! diff --git a/templates/users/sponsorship_detail.html b/templates/users/sponsorship_detail.html index 6c9bceb13..447b4245c 100644 --- a/templates/users/sponsorship_detail.html +++ b/templates/users/sponsorship_detail.html @@ -93,14 +93,14 @@

Application Data

{% if required_assets or fulfilled_assets %}

Required Assets

-

You've selected benefits which requires extra assets (logos, slides etc).

+

You've selected benefits which requires extra assets (logos, slides etc) in order to be fulfilled.


    {% for asset in required_assets %} -
  • {{ asset.label }}: pending. Add asset.
  • +
  • {{ asset.label }}
    Incomplete{% if asset.due_date %} Required by {{ asset.due_date }}{% endif %}: Add asset.
  • {% endfor %} {% for asset in fulfilled_assets %} -
  • {{ asset.label }}: fulfilled. Edit asset.
  • +
  • {{ asset.label }}
    Fulfilled: Edit asset.
  • {% endfor %}

From 98d597308f2bb652224024ab1c87e05b7b8e5725 Mon Sep 17 00:00:00 2001 From: Ee Durbin Date: Tue, 25 Jan 2022 15:16:43 -0500 Subject: [PATCH 13/13] add max_length to RequiredTextAssets, render form to suit --- .../migrations/0072_auto_20220125_2005.py | 23 +++++++++++++++++++ sponsors/models/benefits.py | 12 +++++++++- sponsors/tests/test_models.py | 9 ++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 sponsors/migrations/0072_auto_20220125_2005.py diff --git a/sponsors/migrations/0072_auto_20220125_2005.py b/sponsors/migrations/0072_auto_20220125_2005.py new file mode 100644 index 000000000..86247bc08 --- /dev/null +++ b/sponsors/migrations/0072_auto_20220125_2005.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.24 on 2022-01-25 20:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0071_auto_20220113_1843'), + ] + + operations = [ + migrations.AddField( + model_name='requiredtextasset', + name='max_length', + field=models.IntegerField(blank=True, default=None, help_text='Limit to length of the input, empty means unlimited', null=True), + ), + migrations.AddField( + model_name='requiredtextassetconfiguration', + name='max_length', + field=models.IntegerField(blank=True, default=None, help_text='Limit to length of the input, empty means unlimited', null=True), + ), + ] diff --git a/sponsors/models/benefits.py b/sponsors/models/benefits.py index fb5ec6ec6..a6eadce6a 100644 --- a/sponsors/models/benefits.py +++ b/sponsors/models/benefits.py @@ -163,6 +163,12 @@ class BaseRequiredTextAsset(BaseRequiredAsset): default="", blank=True ) + max_length = models.IntegerField( + default=None, + help_text="Limit to length of the input, empty means unlimited", + null=True, + blank=True, + ) class Meta(BaseRequiredAsset.Meta): abstract = True @@ -534,7 +540,11 @@ def as_form_field(self, **kwargs): help_text = kwargs.pop("help_text", self.help_text) label = kwargs.pop("label", self.label) required = kwargs.pop("required", False) - return forms.CharField(required=required, help_text=help_text, label=label, widget=forms.TextInput, **kwargs) + max_length = self.max_length + widget = forms.TextInput + if max_length is None or max_length > 256: + widget = forms.Textarea + return forms.CharField(required=required, help_text=help_text, label=label, widget=widget, **kwargs) class ProvidedTextAsset(ProvidedAssetMixin, BaseProvidedTextAsset, BenefitFeature): diff --git a/sponsors/tests/test_models.py b/sponsors/tests/test_models.py index 0822a9efa..507bc1b57 100644 --- a/sponsors/tests/test_models.py +++ b/sponsors/tests/test_models.py @@ -976,6 +976,15 @@ def test_build_form_field_from_input(self): text_asset = baker.make(RequiredTextAsset, _fill_optional=True) field = text_asset.as_form_field() self.assertIsInstance(field, forms.CharField) + self.assertIsInstance(field.widget, forms.Textarea) + self.assertFalse(field.required) + self.assertEqual(text_asset.help_text, field.help_text) + self.assertEqual(text_asset.label, field.label) + + def test_build_form_field_from_input_with_max_length(self): + text_asset = baker.make(RequiredTextAsset, _fill_optional=True, max_length=256) + field = text_asset.as_form_field() + self.assertIsInstance(field, forms.CharField) self.assertIsInstance(field.widget, forms.TextInput) self.assertFalse(field.required) self.assertEqual(text_asset.help_text, field.help_text)