Skip to content

Commit 64c6da4

Browse files
authored
Updates to Sponsors app from review. (#1968)
* update account nav to include Sponsorships and remove some old cruft * make labels on asset form more visible * update notification templates for sponsor deadline notifications * document available template tags for sponsorship notification emails * render file assets as a download link * add a "RequiredResponse" for yes/no questions of sponsors
1 parent e2be8e2 commit 64c6da4

14 files changed

+195
-26
lines changed

sponsors/admin.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ class RequiredImgAssetConfigurationInline(StackedPolymorphicInline.Child):
7575
class RequiredTextAssetConfigurationInline(StackedPolymorphicInline.Child):
7676
model = RequiredTextAssetConfiguration
7777

78+
class RequiredResponseAssetConfigurationInline(StackedPolymorphicInline.Child):
79+
model = RequiredResponseAssetConfiguration
80+
7881
class ProvidedTextAssetConfigurationInline(StackedPolymorphicInline.Child):
7982
model = ProvidedTextAssetConfiguration
8083

@@ -88,6 +91,7 @@ class ProvidedFileAssetConfigurationInline(StackedPolymorphicInline.Child):
8891
EmailTargetableConfigurationInline,
8992
RequiredImgAssetConfigurationInline,
9093
RequiredTextAssetConfigurationInline,
94+
RequiredResponseAssetConfigurationInline,
9195
ProvidedTextAssetConfigurationInline,
9296
ProvidedFileAssetConfigurationInline,
9397
]
@@ -692,4 +696,9 @@ def nullify_contract_view(self, request, pk):
692696

693697
@admin.register(SponsorEmailNotificationTemplate)
694698
class SponsorEmailNotificationTemplateAdmin(BaseEmailTemplateAdmin):
695-
pass
699+
def get_form(self, request, obj=None, **kwargs):
700+
help_texts = {
701+
"content": SPONSOR_TEMPLATE_HELP_TEXT,
702+
}
703+
kwargs.update({"help_texts": help_texts})
704+
return super().get_form(request, obj, **kwargs)

sponsors/forms.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
SponsorEmailNotificationTemplate,
2323
RequiredImgAssetConfiguration,
2424
BenefitFeature,
25+
SPONSOR_TEMPLATE_HELP_TEXT,
2526
)
2627

2728

@@ -487,7 +488,11 @@ class SendSponsorshipNotificationForm(forms.Form):
487488
required=False,
488489
)
489490
subject = forms.CharField(max_length=140, required=False)
490-
content = forms.CharField(widget=forms.widgets.Textarea(), required=False)
491+
content = forms.CharField(
492+
widget=forms.widgets.Textarea(),
493+
required=False,
494+
help_text=SPONSOR_TEMPLATE_HELP_TEXT,
495+
)
491496

492497
def clean(self):
493498
cleaned_data = super().clean()
@@ -629,9 +634,9 @@ def __init__(self, *args, **kwargs):
629634
field = required_asset.as_form_field(required=required, initial=value)
630635

631636
if required_asset.due_date and not bool(value):
632-
field.label = mark_safe(f"{field.label} <b>(Required by {required_asset.due_date})</b>")
637+
field.label = mark_safe(f"<big><b>{field.label}</b></big><br><b>(Required by {required_asset.due_date})</b>")
633638
if bool(value):
634-
field.label = mark_safe(f"{field.label} <small>(Fulfilled, thank you!)</small>")
639+
field.label = mark_safe(f"<big><b>{field.label}</b></big><br><small>(Fulfilled, thank you!)</small>")
635640

636641
fields[f_name] = field
637642

sponsors/management/commands/check_sponsorship_assets_due_date.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def handle(self, **options):
5858

5959
notification = AssetCloseToDueDateNotificationToSponsors()
6060
for sponsorship in sponsorships_to_notify:
61-
kwargs = {"sponsorship": sponsorship, "due_date": target_date}
61+
kwargs = {"sponsorship": sponsorship, "days": num_days, "due_date": target_date}
6262
notification.notify(**kwargs)
6363

6464
print("Notifications sent!")
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Generated by Django 2.2.24 on 2022-01-28 19:06
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import sponsors.models.benefits
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('sponsors', '0072_auto_20220125_2005'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='RequiredResponseAsset',
17+
fields=[
18+
('benefitfeature_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sponsors.BenefitFeature')),
19+
('related_to', models.CharField(choices=[('sponsor', 'Sponsor'), ('sponsorship', 'Sponsorship')], help_text='To which instance (Sponsor or Sponsorship) should this asset relate to.', max_length=30, verbose_name='Related To')),
20+
('internal_name', models.CharField(db_index=True, help_text='Unique name used internally to control if the sponsor/sponsorship already has the asset', max_length=128, verbose_name='Internal Name')),
21+
('label', models.CharField(help_text="What's the title used to display the input to the sponsor?", max_length=256)),
22+
('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the input should be populated', max_length=256)),
23+
('due_date', models.DateField(blank=True, default=None, null=True)),
24+
],
25+
options={
26+
'verbose_name': 'Require Response',
27+
'verbose_name_plural': 'Required Responses',
28+
'abstract': False,
29+
'base_manager_name': 'objects',
30+
},
31+
bases=(sponsors.models.benefits.RequiredAssetMixin, 'sponsors.benefitfeature', models.Model),
32+
),
33+
migrations.CreateModel(
34+
name='RequiredResponseAssetConfiguration',
35+
fields=[
36+
('benefitfeatureconfiguration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sponsors.BenefitFeatureConfiguration')),
37+
('related_to', models.CharField(choices=[('sponsor', 'Sponsor'), ('sponsorship', 'Sponsorship')], help_text='To which instance (Sponsor or Sponsorship) should this asset relate to.', max_length=30, verbose_name='Related To')),
38+
('internal_name', models.CharField(db_index=True, help_text='Unique name used internally to control if the sponsor/sponsorship already has the asset', max_length=128, verbose_name='Internal Name')),
39+
('label', models.CharField(help_text="What's the title used to display the input to the sponsor?", max_length=256)),
40+
('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the input should be populated', max_length=256)),
41+
('due_date', models.DateField(blank=True, default=None, null=True)),
42+
],
43+
options={
44+
'verbose_name': 'Require Response Configuration',
45+
'verbose_name_plural': 'Require Response Configurations',
46+
'abstract': False,
47+
'base_manager_name': 'objects',
48+
},
49+
bases=(sponsors.models.benefits.AssetConfigurationMixin, 'sponsors.benefitfeatureconfiguration', models.Model),
50+
),
51+
migrations.CreateModel(
52+
name='ResponseAsset',
53+
fields=[
54+
('genericasset_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sponsors.GenericAsset')),
55+
('response', models.CharField(choices=[('YES', 'Yes'), ('NO', 'No')], max_length=32, null=True)),
56+
],
57+
options={
58+
'verbose_name': 'Response Asset',
59+
'verbose_name_plural': 'Response Assets',
60+
},
61+
bases=('sponsors.genericasset',),
62+
),
63+
migrations.AddConstraint(
64+
model_name='requiredresponseassetconfiguration',
65+
constraint=models.UniqueConstraint(fields=('internal_name',), name='uniq_response_asset_cfg'),
66+
),
67+
]

sponsors/models/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
"""
66

77
from .assets import GenericAsset, ImgAsset, TextAsset
8-
from .notifications import SponsorEmailNotificationTemplate
8+
from .notifications import SponsorEmailNotificationTemplate, SPONSOR_TEMPLATE_HELP_TEXT
99
from .sponsors import Sponsor, SponsorContact, SponsorBenefit
1010
from .benefits import BaseLogoPlacement, BaseTieredQuantity, BaseEmailTargetable, BenefitFeatureConfiguration, \
1111
LogoPlacementConfiguration, TieredQuantityConfiguration, EmailTargetableConfiguration, BenefitFeature, \
1212
LogoPlacement, EmailTargetable, TieredQuantity, RequiredImgAsset, RequiredImgAssetConfiguration, \
13-
RequiredTextAssetConfiguration, RequiredTextAsset, ProvidedTextAssetConfiguration, ProvidedTextAsset, \
14-
ProvidedFileAssetConfiguration, ProvidedFileAsset
13+
RequiredTextAssetConfiguration, RequiredTextAsset, RequiredResponseAssetConfiguration, RequiredResponseAsset, \
14+
ProvidedTextAssetConfiguration, ProvidedTextAsset, ProvidedFileAssetConfiguration, ProvidedFileAsset
1515
from .sponsorship import Sponsorship, SponsorshipProgram, SponsorshipBenefit, Sponsorship, SponsorshipPackage
1616
from .contract import LegalClause, Contract, signed_contract_random_path

sponsors/models/assets.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from Sponsors or Sponsorships
44
"""
55
import uuid
6+
from enum import Enum
67
from pathlib import Path
78

89
from django.db import models
@@ -25,13 +26,14 @@ class GenericAsset(PolymorphicModel):
2526
"""
2627
Base class used to add required assets to Sponsor or Sponsorship objects
2728
"""
29+
2830
# UUID can't be the object ID because Polymorphic expects default django integer ID
2931
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
3032
# The next 3 fields are required by Django to enable and set up generic relations
3133
# pointing the asset to a Sponsor or Sponsorship object
3234
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
3335
object_id = models.PositiveIntegerField()
34-
content_object = GenericForeignKey('content_type', 'object_id')
36+
content_object = GenericForeignKey("content_type", "object_id")
3537

3638
# must match with internal_name from benefits configuration which describe assets
3739
internal_name = models.CharField(
@@ -113,3 +115,33 @@ def value(self):
113115
@value.setter
114116
def value(self, value):
115117
self.file = value
118+
119+
120+
class Response(Enum):
121+
YES = "Yes"
122+
NO = "No"
123+
124+
@classmethod
125+
def choices(cls):
126+
return tuple((i.name, i.value) for i in cls)
127+
128+
129+
class ResponseAsset(GenericAsset):
130+
response = models.CharField(
131+
max_length=32, choices=Response.choices(), blank=False, null=True
132+
)
133+
134+
def __str__(self):
135+
return f"Response Asset: {self.internal_name}"
136+
137+
class Meta:
138+
verbose_name = "Response Asset"
139+
verbose_name_plural = "Response Assets"
140+
141+
@property
142+
def value(self):
143+
return self.response
144+
145+
@value.setter
146+
def value(self, value):
147+
self.response = value

sponsors/models/benefits.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
from django.urls import reverse
88
from polymorphic.models import PolymorphicModel
99

10-
from sponsors.models.assets import ImgAsset, TextAsset, FileAsset
11-
from sponsors.models.enums import PublisherChoices, LogoPlacementChoices, AssetsRelatedTo
10+
from sponsors.models.assets import ImgAsset, TextAsset, FileAsset, ResponseAsset, Response
11+
from sponsors.models.enums import (
12+
PublisherChoices,
13+
LogoPlacementChoices,
14+
AssetsRelatedTo,
15+
)
1216

1317
########################################
1418
# Benefit features abstract classes
@@ -174,6 +178,13 @@ class Meta(BaseRequiredAsset.Meta):
174178
abstract = True
175179

176180

181+
class BaseRequiredResponseAsset(BaseRequiredAsset):
182+
ASSET_CLASS = ResponseAsset
183+
184+
class Meta(BaseRequiredAsset.Meta):
185+
abstract = True
186+
187+
177188
class BaseProvidedTextAsset(BaseProvidedAsset):
178189
ASSET_CLASS = TextAsset
179190

@@ -423,8 +434,27 @@ def benefit_feature_class(self):
423434
return RequiredTextAsset
424435

425436

426-
class ProvidedTextAssetConfiguration(AssetConfigurationMixin, BaseProvidedTextAsset,
427-
BenefitFeatureConfiguration):
437+
class RequiredResponseAssetConfiguration(
438+
AssetConfigurationMixin, BaseRequiredResponseAsset, BenefitFeatureConfiguration
439+
):
440+
class Meta(BaseRequiredResponseAsset.Meta, BenefitFeatureConfiguration.Meta):
441+
verbose_name = "Require Response Configuration"
442+
verbose_name_plural = "Require Response Configurations"
443+
constraints = [
444+
UniqueConstraint(fields=["internal_name"], name="uniq_response_asset_cfg")
445+
]
446+
447+
def __str__(self):
448+
return f"Require response configuration"
449+
450+
@property
451+
def benefit_feature_class(self):
452+
return RequiredResponseAsset
453+
454+
455+
class ProvidedTextAssetConfiguration(
456+
AssetConfigurationMixin, BaseProvidedTextAsset, BenefitFeatureConfiguration
457+
):
428458
class Meta(BaseProvidedTextAsset.Meta, BenefitFeatureConfiguration.Meta):
429459
verbose_name = "Provided Text Configuration"
430460
verbose_name_plural = "Provided Text Configurations"
@@ -547,6 +577,21 @@ def as_form_field(self, **kwargs):
547577
return forms.CharField(required=required, help_text=help_text, label=label, widget=widget, **kwargs)
548578

549579

580+
class RequiredResponseAsset(RequiredAssetMixin, BaseRequiredResponseAsset, BenefitFeature):
581+
class Meta(BaseRequiredTextAsset.Meta, BenefitFeature.Meta):
582+
verbose_name = "Require Response"
583+
verbose_name_plural = "Required Responses"
584+
585+
def __str__(self):
586+
return f"Require response"
587+
588+
def as_form_field(self, **kwargs):
589+
help_text = kwargs.pop("help_text", self.help_text)
590+
label = kwargs.pop("label", self.label)
591+
required = kwargs.pop("required", False)
592+
return forms.ChoiceField(required=required, choices=Response.choices(), widget=forms.RadioSelect, help_text=help_text, label=label, **kwargs)
593+
594+
550595
class ProvidedTextAsset(ProvidedAssetMixin, BaseProvidedTextAsset, BenefitFeature):
551596
class Meta(BaseProvidedTextAsset.Meta, BenefitFeature.Meta):
552597
verbose_name = "Provided Text"

sponsors/models/notifications.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
from mailing.models import BaseEmailTemplate
44

5+
SPONSOR_TEMPLATE_HELP_TEXT = (
6+
"<br>"
7+
"You can use the following template variables in the Subject and Content:"
8+
" <pre>{{ sponsor_name }}</pre>"
9+
" <pre>{{ sponsorship_level }}</pre>"
10+
" <pre>{{ sponsorship_start_date }}</pre>"
11+
" <pre>{{ sponsorship_end_date }}</pre>"
12+
" <pre>{{ sponsorship_status }}</pre>"
13+
)
14+
515

616
#################################
717
# Sponsor Email Notifications

sponsors/notifications.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def notify(self, *args, **kwargs):
221221
class AssetCloseToDueDateNotificationToSponsors(BaseEmailSponsorshipNotification):
222222
subject_template = "sponsors/email/sponsor_expiring_assets_subject.txt"
223223
message_template = "sponsors/email/sponsor_expiring_assets.txt"
224-
email_context_keys = ["sponsorship", "required_assets", "due_date"]
224+
email_context_keys = ["sponsorship", "required_assets", "due_date", "days"]
225225

226226
def get_recipient_list(self, context):
227227
return context["sponsorship"].verified_emails

sponsors/tests/test_notifications.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -455,11 +455,11 @@ def setUp(self):
455455
self.content_template = "sponsors/email/sponsor_expiring_assets.txt"
456456

457457
def test_send_email_using_correct_templates(self):
458-
context = {"sponsorship": self.sponsorship}
458+
context = {"sponsorship": self.sponsorship, "days": 7}
459459
expected_subject = render_to_string(self.subject_template, context).strip()
460460
expected_content = render_to_string(self.content_template, context).strip()
461461

462-
self.notification.notify(sponsorship=self.sponsorship)
462+
self.notification.notify(sponsorship=self.sponsorship, days=7)
463463
self.assertTrue(mail.outbox)
464464

465465
email = mail.outbox[0]
@@ -479,9 +479,9 @@ def test_list_required_assets_in_email_context(self):
479479
cfg = baker.make(RequiredTextAssetConfiguration, internal_name='input')
480480
benefit = baker.make(SponsorBenefit, sponsorship=self.sponsorship)
481481
asset = cfg.create_benefit_feature(benefit)
482-
base_context = {"sponsorship": self.sponsorship, "due_date": date.today()}
482+
base_context = {"sponsorship": self.sponsorship, "due_date": date.today(), "days": 7}
483483
context = self.notification.get_email_context(**base_context)
484-
self.assertEqual(3, len(context))
484+
self.assertEqual(4, len(context))
485485
self.assertEqual(self.sponsorship, context["sponsorship"])
486486
self.assertEqual(1, len(context["required_assets"]))
487487
self.assertEqual(date.today(), context["due_date"])

0 commit comments

Comments
 (0)