Skip to content

Commit b4efb1e

Browse files
berinhardewdurbin
andauthored
Enable to have sponsorship applications only with a la carte benefits (#1946)
* Add boolean field to flag a la carte benefits * Fix typo in form name * Display a la carte boolean and admin page and raise error if a la carte with associated package * Add filter for every benefit boolean flag * Add new field to form to list a la carte benefits * Make sure package is required, but not if submission only with a la carte * Remove unecessary end of regex and avoid warning message * Add slug to sponsorship packages to make it easier to uniquely reference them * Remove default value after running data migration * Assign a custom package if sponsorship application only with a la carte benefits * Make sure the view can handle a la carte only applications * Update sponsorships application form to list a la carte benefits * Propagate a la carte flag on SponsorBenefit objects * update all step titles in sponsorship form to a new svg plus bonus step if ever needed :-D Co-authored-by: Ee Durbin <ewdurbin@gmail.com>
1 parent 5822685 commit b4efb1e

29 files changed

+483
-88
lines changed

mailing/admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def get_urls(self):
3434
prefix = self.model._meta.db_table
3535
my_urls = [
3636
path(
37-
"<int:pk>/preview-content/$",
37+
"<int:pk>/preview-content/",
3838
self.admin_site.admin_view(self.preview_email_template),
3939
name=f"{prefix}_preview",
4040
),

sponsors/admin.py

+16-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
from mailing.admin import BaseEmailTemplateAdmin
1414
from sponsors.models import *
1515
from sponsors import views_admin
16-
from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm, RequiredImgAssetConfigurationForm
16+
from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm, RequiredImgAssetConfigurationForm, \
17+
SponsorshipBenefitAdminForm
1718
from cms.admin import ContentManageableModelAdmin
1819

1920

@@ -88,8 +89,9 @@ class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin):
8889
"internal_value",
8990
"move_up_down_links",
9091
]
91-
list_filter = ["program", "package_only", "packages"]
92+
list_filter = ["program", "package_only", "packages", "new", "a_la_carte", "unavailable"]
9293
search_fields = ["name"]
94+
form = SponsorshipBenefitAdminForm
9395

9496
fieldsets = [
9597
(
@@ -103,6 +105,7 @@ class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin):
103105
"package_only",
104106
"new",
105107
"unavailable",
108+
"a_la_carte",
106109
),
107110
},
108111
),
@@ -144,9 +147,17 @@ class SponsorshipPackageAdmin(OrderedModelAdmin):
144147
search_fields = ["name"]
145148

146149
def get_readonly_fields(self, request, obj=None):
147-
if request.user.is_superuser:
148-
return []
149-
return ["logo_dimension"]
150+
readonly = []
151+
if obj:
152+
readonly.append("slug")
153+
if not request.user.is_superuser:
154+
readonly.append("logo_dimension")
155+
return readonly
156+
157+
def get_prepopulated_fields(self, request, obj=None):
158+
if not obj:
159+
return {'slug': ['name']}
160+
return {}
150161

151162

152163
class SponsorContactInline(admin.TabularInline):

sponsors/forms.py

+55-7
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ class Meta:
4747
)
4848

4949

50-
class SponsorshiptBenefitsForm(forms.Form):
50+
class SponsorshipsBenefitsForm(forms.Form):
51+
"""
52+
Form to enable user to select packages, benefits and add-ons during
53+
the sponsorship application submission.
54+
"""
5155
package = forms.ModelChoiceField(
5256
queryset=SponsorshipPackage.objects.list_advertisables(),
5357
widget=forms.RadioSelect(),
@@ -58,6 +62,10 @@ class SponsorshiptBenefitsForm(forms.Form):
5862
required=False,
5963
queryset=SponsorshipBenefit.objects.add_ons().select_related("program"),
6064
)
65+
a_la_carte_benefits = PickSponsorshipBenefitsField(
66+
required=False,
67+
queryset=SponsorshipBenefit.objects.a_la_carte().select_related("program"),
68+
)
6169

6270
def __init__(self, *args, **kwargs):
6371
super().__init__(*args, **kwargs)
@@ -89,18 +97,31 @@ def benefits_conflicts(self):
8997
conflicts[benefit.id] = list(benefits_conflicts)
9098
return conflicts
9199

92-
def get_benefits(self, cleaned_data=None, include_add_ons=False):
100+
def get_benefits(self, cleaned_data=None, include_add_ons=False, include_a_la_carte=False):
93101
cleaned_data = cleaned_data or self.cleaned_data
94102
benefits = list(
95103
chain(*(cleaned_data.get(bp.name) for bp in self.benefits_programs))
96104
)
97-
add_ons = cleaned_data.get("add_ons_benefits")
98-
if include_add_ons and add_ons:
105+
add_ons = cleaned_data.get("add_ons_benefits", [])
106+
if include_add_ons:
99107
benefits.extend([b for b in add_ons])
108+
a_la_carte = cleaned_data.get("a_la_carte_benefits", [])
109+
if include_a_la_carte:
110+
benefits.extend([b for b in a_la_carte])
100111
return benefits
101112

102113
def get_package(self):
103-
return self.cleaned_data.get("package")
114+
pkg = self.cleaned_data.get("package")
115+
116+
pkg_benefits = self.get_benefits(include_add_ons=True)
117+
a_la_carte = self.cleaned_data.get("a_la_carte_benefits")
118+
if not pkg_benefits and a_la_carte: # a la carte only
119+
pkg, _ = SponsorshipPackage.objects.get_or_create(
120+
slug="a-la-carte-only",
121+
defaults={"name": "A La Carte Only", "sponsorship_amount": 0},
122+
)
123+
124+
return pkg
104125

105126
def _clean_benefits(self, cleaned_data):
106127
"""
@@ -110,11 +131,17 @@ def _clean_benefits(self, cleaned_data):
110131
- benefit with no capacity, except if soft
111132
"""
112133
package = cleaned_data.get("package")
113-
benefits = self.get_benefits(cleaned_data)
114-
if not benefits:
134+
benefits = self.get_benefits(cleaned_data, include_add_ons=True)
135+
a_la_carte = cleaned_data.get("a_la_carte_benefits")
136+
137+
if not benefits and not a_la_carte:
115138
raise forms.ValidationError(
116139
_("You have to pick a minimum number of benefits.")
117140
)
141+
elif benefits and not package:
142+
raise forms.ValidationError(
143+
_("You must pick a package to include the selected benefits.")
144+
)
118145

119146
benefits_ids = [b.id for b in benefits]
120147
for benefit in benefits:
@@ -408,6 +435,8 @@ def save(self, commit=True):
408435
self.instance.name = benefit.name
409436
self.instance.description = benefit.description
410437
self.instance.program = benefit.program
438+
self.instance.added_by_user = self.instance.added_by_user or benefit.a_la_carte
439+
self.instance.a_la_carte = benefit.a_la_carte
411440

412441
if commit:
413442
self.instance.save()
@@ -611,3 +640,22 @@ def update_assets(self):
611640
@property
612641
def has_input(self):
613642
return bool(self.fields)
643+
644+
645+
class SponsorshipBenefitAdminForm(forms.ModelForm):
646+
647+
class Meta:
648+
model = SponsorshipBenefit
649+
fields = "__all__"
650+
651+
def clean(self):
652+
cleaned_data = super().clean()
653+
a_la_carte = cleaned_data.get("a_la_carte")
654+
packages = cleaned_data.get("packages")
655+
656+
# a la carte benefit cannot be associated with a package
657+
if a_la_carte and packages:
658+
error = "À la carte benefits must not belong to any package."
659+
raise forms.ValidationError(error)
660+
661+
return cleaned_data
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 2.2.24 on 2021-12-20 14:22
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('sponsors', '0062_auto_20211111_1529'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='sponsorshipbenefit',
15+
name='a_la_carte',
16+
field=models.BooleanField(default=False, help_text='À la carte benefits can be selected without the need of a package.', verbose_name='À La Carte'),
17+
),
18+
migrations.AlterField(
19+
model_name='requiredtextasset',
20+
name='label',
21+
field=models.CharField(help_text="What's the title used to display the text input to the sponsor?", max_length=256),
22+
),
23+
migrations.AlterField(
24+
model_name='requiredtextassetconfiguration',
25+
name='label',
26+
field=models.CharField(help_text="What's the title used to display the text input to the sponsor?", max_length=256),
27+
),
28+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.24 on 2021-12-23 13:08
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('sponsors', '0063_auto_20211220_1422'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='sponsorshippackage',
15+
name='slug',
16+
field=models.SlugField(default='', help_text='Internal identifier used to reference this package.'),
17+
),
18+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 2.2.24 on 2021-12-23 13:09
2+
3+
from django.db import migrations
4+
from django.utils.text import slugify
5+
6+
7+
def populate_packages_slugs(apps, schema_editor):
8+
SponsorshipPackage = apps.get_model("sponsors", "SponsorshipPackage")
9+
qs = SponsorshipPackage.objects.filter(slug="")
10+
for pkg in qs:
11+
pkg.slug = slugify(pkg.name)
12+
pkg.save()
13+
14+
15+
class Migration(migrations.Migration):
16+
17+
dependencies = [
18+
('sponsors', '0064_sponsorshippackage_slug'),
19+
]
20+
21+
operations = [
22+
migrations.RunPython(populate_packages_slugs, migrations.RunPython.noop)
23+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.24 on 2021-12-23 13:18
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('sponsors', '0065_auto_20211223_1309'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='sponsorshippackage',
15+
name='slug',
16+
field=models.SlugField(help_text='Internal identifier used to reference this package.'),
17+
),
18+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.24 on 2021-12-24 14:21
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('sponsors', '0066_auto_20211223_1318'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='sponsorbenefit',
15+
name='a_la_carte',
16+
field=models.BooleanField(blank=True, default=False, verbose_name='Added as a la carte benefit?'),
17+
),
18+
]

sponsors/models/managers.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,15 @@ def without_conflicts(self):
8080
return self.filter(conflicts__isnull=True)
8181

8282
def add_ons(self):
83-
return self.annotate(num_packages=Count("packages")).filter(num_packages=0)
83+
return self.annotate(num_packages=Count("packages")).filter(num_packages=0, a_la_carte=False)
84+
85+
def a_la_carte(self):
86+
return self.filter(a_la_carte=True)
8487

8588
def with_packages(self):
8689
return (
8790
self.annotate(num_packages=Count("packages"))
88-
.exclude(num_packages=0)
91+
.exclude(Q(num_packages=0) | Q(a_la_carte=True))
8992
.order_by("-num_packages", "order")
9093
)
9194

sponsors/models/sponsors.py

+5
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ class SponsorBenefit(OrderedModel):
206206
added_by_user = models.BooleanField(
207207
blank=True, default=False, verbose_name="Added by user?"
208208
)
209+
a_la_carte = models.BooleanField(
210+
blank=True, default=False, verbose_name="Added as a la carte benefit?"
211+
)
209212

210213
def __str__(self):
211214
if self.program is not None:
@@ -218,6 +221,8 @@ def features(self):
218221

219222
@classmethod
220223
def new_copy(cls, benefit, **kwargs):
224+
kwargs["added_by_user"] = kwargs.get("added_by_user") or benefit.a_la_carte
225+
kwargs["a_la_carte"] = benefit.a_la_carte
221226
sponsor_benefit = cls.objects.create(
222227
sponsorship_benefit=benefit,
223228
program_name=benefit.program.name,

sponsors/models/sponsorship.py

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class SponsorshipPackage(OrderedModel):
3838
logo_dimension = models.PositiveIntegerField(default=175, blank=True, help_text="Internal value used to control "
3939
"logos dimensions at sponsors "
4040
"page")
41+
slug = models.SlugField(db_index=True, blank=False, null=False, help_text="Internal identifier used "
42+
"to reference this package.")
4143

4244
def __str__(self):
4345
return self.name
@@ -367,6 +369,11 @@ class SponsorshipBenefit(OrderedModel):
367369
verbose_name="Benefit is unavailable",
368370
help_text="If selected, this benefit will not be available to applicants.",
369371
)
372+
a_la_carte = models.BooleanField(
373+
default=False,
374+
verbose_name="À La Carte",
375+
help_text="À la carte benefits can be selected without the need of a package.",
376+
)
370377

371378
# Internal
372379
legal_clauses = models.ManyToManyField(

0 commit comments

Comments
 (0)