Skip to content

Commit

Permalink
Enable to have sponsorship applications only with a la carte benefits (
Browse files Browse the repository at this point in the history
…#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 <[email protected]>
  • Loading branch information
berinhard and ewdurbin authored Jan 5, 2022
1 parent 5822685 commit b4efb1e
Show file tree
Hide file tree
Showing 29 changed files with 483 additions and 88 deletions.
2 changes: 1 addition & 1 deletion mailing/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get_urls(self):
prefix = self.model._meta.db_table
my_urls = [
path(
"<int:pk>/preview-content/$",
"<int:pk>/preview-content/",
self.admin_site.admin_view(self.preview_email_template),
name=f"{prefix}_preview",
),
Expand Down
21 changes: 16 additions & 5 deletions sponsors/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from mailing.admin import BaseEmailTemplateAdmin
from sponsors.models import *
from sponsors import views_admin
from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm, RequiredImgAssetConfigurationForm
from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm, RequiredImgAssetConfigurationForm, \
SponsorshipBenefitAdminForm
from cms.admin import ContentManageableModelAdmin


Expand Down Expand Up @@ -88,8 +89,9 @@ class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin):
"internal_value",
"move_up_down_links",
]
list_filter = ["program", "package_only", "packages"]
list_filter = ["program", "package_only", "packages", "new", "a_la_carte", "unavailable"]
search_fields = ["name"]
form = SponsorshipBenefitAdminForm

fieldsets = [
(
Expand All @@ -103,6 +105,7 @@ class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin):
"package_only",
"new",
"unavailable",
"a_la_carte",
),
},
),
Expand Down Expand Up @@ -144,9 +147,17 @@ class SponsorshipPackageAdmin(OrderedModelAdmin):
search_fields = ["name"]

def get_readonly_fields(self, request, obj=None):
if request.user.is_superuser:
return []
return ["logo_dimension"]
readonly = []
if obj:
readonly.append("slug")
if not request.user.is_superuser:
readonly.append("logo_dimension")
return readonly

def get_prepopulated_fields(self, request, obj=None):
if not obj:
return {'slug': ['name']}
return {}


class SponsorContactInline(admin.TabularInline):
Expand Down
62 changes: 55 additions & 7 deletions sponsors/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ class Meta:
)


class SponsorshiptBenefitsForm(forms.Form):
class SponsorshipsBenefitsForm(forms.Form):
"""
Form to enable user to select packages, benefits and add-ons during
the sponsorship application submission.
"""
package = forms.ModelChoiceField(
queryset=SponsorshipPackage.objects.list_advertisables(),
widget=forms.RadioSelect(),
Expand All @@ -58,6 +62,10 @@ class SponsorshiptBenefitsForm(forms.Form):
required=False,
queryset=SponsorshipBenefit.objects.add_ons().select_related("program"),
)
a_la_carte_benefits = PickSponsorshipBenefitsField(
required=False,
queryset=SponsorshipBenefit.objects.a_la_carte().select_related("program"),
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -89,18 +97,31 @@ def benefits_conflicts(self):
conflicts[benefit.id] = list(benefits_conflicts)
return conflicts

def get_benefits(self, cleaned_data=None, include_add_ons=False):
def get_benefits(self, cleaned_data=None, include_add_ons=False, include_a_la_carte=False):
cleaned_data = cleaned_data or self.cleaned_data
benefits = list(
chain(*(cleaned_data.get(bp.name) for bp in self.benefits_programs))
)
add_ons = cleaned_data.get("add_ons_benefits")
if include_add_ons and add_ons:
add_ons = cleaned_data.get("add_ons_benefits", [])
if include_add_ons:
benefits.extend([b for b in add_ons])
a_la_carte = cleaned_data.get("a_la_carte_benefits", [])
if include_a_la_carte:
benefits.extend([b for b in a_la_carte])
return benefits

def get_package(self):
return self.cleaned_data.get("package")
pkg = self.cleaned_data.get("package")

pkg_benefits = self.get_benefits(include_add_ons=True)
a_la_carte = self.cleaned_data.get("a_la_carte_benefits")
if not pkg_benefits and a_la_carte: # a la carte only
pkg, _ = SponsorshipPackage.objects.get_or_create(
slug="a-la-carte-only",
defaults={"name": "A La Carte Only", "sponsorship_amount": 0},
)

return pkg

def _clean_benefits(self, cleaned_data):
"""
Expand All @@ -110,11 +131,17 @@ def _clean_benefits(self, cleaned_data):
- benefit with no capacity, except if soft
"""
package = cleaned_data.get("package")
benefits = self.get_benefits(cleaned_data)
if not benefits:
benefits = self.get_benefits(cleaned_data, include_add_ons=True)
a_la_carte = cleaned_data.get("a_la_carte_benefits")

if not benefits and not a_la_carte:
raise forms.ValidationError(
_("You have to pick a minimum number of benefits.")
)
elif benefits and not package:
raise forms.ValidationError(
_("You must pick a package to include the selected benefits.")
)

benefits_ids = [b.id for b in benefits]
for benefit in benefits:
Expand Down Expand Up @@ -408,6 +435,8 @@ def save(self, commit=True):
self.instance.name = benefit.name
self.instance.description = benefit.description
self.instance.program = benefit.program
self.instance.added_by_user = self.instance.added_by_user or benefit.a_la_carte
self.instance.a_la_carte = benefit.a_la_carte

if commit:
self.instance.save()
Expand Down Expand Up @@ -611,3 +640,22 @@ def update_assets(self):
@property
def has_input(self):
return bool(self.fields)


class SponsorshipBenefitAdminForm(forms.ModelForm):

class Meta:
model = SponsorshipBenefit
fields = "__all__"

def clean(self):
cleaned_data = super().clean()
a_la_carte = cleaned_data.get("a_la_carte")
packages = cleaned_data.get("packages")

# a la carte benefit cannot be associated with a package
if a_la_carte and packages:
error = "À la carte benefits must not belong to any package."
raise forms.ValidationError(error)

return cleaned_data
28 changes: 28 additions & 0 deletions sponsors/migrations/0063_auto_20211220_1422.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 2.2.24 on 2021-12-20 14:22

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0062_auto_20211111_1529'),
]

operations = [
migrations.AddField(
model_name='sponsorshipbenefit',
name='a_la_carte',
field=models.BooleanField(default=False, help_text='À la carte benefits can be selected without the need of a package.', verbose_name='À La Carte'),
),
migrations.AlterField(
model_name='requiredtextasset',
name='label',
field=models.CharField(help_text="What's the title used to display the text input to the sponsor?", max_length=256),
),
migrations.AlterField(
model_name='requiredtextassetconfiguration',
name='label',
field=models.CharField(help_text="What's the title used to display the text input to the sponsor?", max_length=256),
),
]
18 changes: 18 additions & 0 deletions sponsors/migrations/0064_sponsorshippackage_slug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-12-23 13:08

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0063_auto_20211220_1422'),
]

operations = [
migrations.AddField(
model_name='sponsorshippackage',
name='slug',
field=models.SlugField(default='', help_text='Internal identifier used to reference this package.'),
),
]
23 changes: 23 additions & 0 deletions sponsors/migrations/0065_auto_20211223_1309.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2.24 on 2021-12-23 13:09

from django.db import migrations
from django.utils.text import slugify


def populate_packages_slugs(apps, schema_editor):
SponsorshipPackage = apps.get_model("sponsors", "SponsorshipPackage")
qs = SponsorshipPackage.objects.filter(slug="")
for pkg in qs:
pkg.slug = slugify(pkg.name)
pkg.save()


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0064_sponsorshippackage_slug'),
]

operations = [
migrations.RunPython(populate_packages_slugs, migrations.RunPython.noop)
]
18 changes: 18 additions & 0 deletions sponsors/migrations/0066_auto_20211223_1318.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-12-23 13:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0065_auto_20211223_1309'),
]

operations = [
migrations.AlterField(
model_name='sponsorshippackage',
name='slug',
field=models.SlugField(help_text='Internal identifier used to reference this package.'),
),
]
18 changes: 18 additions & 0 deletions sponsors/migrations/0067_sponsorbenefit_a_la_carte.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-12-24 14:21

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sponsors', '0066_auto_20211223_1318'),
]

operations = [
migrations.AddField(
model_name='sponsorbenefit',
name='a_la_carte',
field=models.BooleanField(blank=True, default=False, verbose_name='Added as a la carte benefit?'),
),
]
7 changes: 5 additions & 2 deletions sponsors/models/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,15 @@ def without_conflicts(self):
return self.filter(conflicts__isnull=True)

def add_ons(self):
return self.annotate(num_packages=Count("packages")).filter(num_packages=0)
return self.annotate(num_packages=Count("packages")).filter(num_packages=0, a_la_carte=False)

def a_la_carte(self):
return self.filter(a_la_carte=True)

def with_packages(self):
return (
self.annotate(num_packages=Count("packages"))
.exclude(num_packages=0)
.exclude(Q(num_packages=0) | Q(a_la_carte=True))
.order_by("-num_packages", "order")
)

Expand Down
5 changes: 5 additions & 0 deletions sponsors/models/sponsors.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ class SponsorBenefit(OrderedModel):
added_by_user = models.BooleanField(
blank=True, default=False, verbose_name="Added by user?"
)
a_la_carte = models.BooleanField(
blank=True, default=False, verbose_name="Added as a la carte benefit?"
)

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

@classmethod
def new_copy(cls, benefit, **kwargs):
kwargs["added_by_user"] = kwargs.get("added_by_user") or benefit.a_la_carte
kwargs["a_la_carte"] = benefit.a_la_carte
sponsor_benefit = cls.objects.create(
sponsorship_benefit=benefit,
program_name=benefit.program.name,
Expand Down
7 changes: 7 additions & 0 deletions sponsors/models/sponsorship.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class SponsorshipPackage(OrderedModel):
logo_dimension = models.PositiveIntegerField(default=175, blank=True, help_text="Internal value used to control "
"logos dimensions at sponsors "
"page")
slug = models.SlugField(db_index=True, blank=False, null=False, help_text="Internal identifier used "
"to reference this package.")

def __str__(self):
return self.name
Expand Down Expand Up @@ -367,6 +369,11 @@ class SponsorshipBenefit(OrderedModel):
verbose_name="Benefit is unavailable",
help_text="If selected, this benefit will not be available to applicants.",
)
a_la_carte = models.BooleanField(
default=False,
verbose_name="À La Carte",
help_text="À la carte benefits can be selected without the need of a package.",
)

# Internal
legal_clauses = models.ManyToManyField(
Expand Down
Loading

0 comments on commit b4efb1e

Please sign in to comment.