Skip to content

Commit 2c6ef18

Browse files
ewdurbinberinhard
andauthored
Sponsor benefit provided assets (#1959)
* add missing migration * WIP: "Provided Assets" from PSF to sponsors creates the ability to configure "Provided Assets" associated with a sponsorship that will be fulfilled by the PSF. The initial "ProvidedTextAsset" is intended to be used for PyCon US 2022 voucher codes, which will be unique to each voucher "type" and sponsorship level. Additionally, we will likely include "ProvidedFileAsset" that will be used for all sponsorships with Expo Hall benefits to share with them a common "Exhibitor Packet" * upload files/shared provided file assets for configured benefits * 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: jazzband/django-polymorphic#229 * Refactor how to reconfigure sponsor benefit to avoid delete operations * Make sure the assets are also being updated * Add docstring to make it more clear that the input assets are decoupled from their configuration * Fix bad context variable name * Fields that configure relationship between assets and configuration should be read only for editing Co-authored-by: Bernardo Fontes <[email protected]>
1 parent 751ed17 commit 2c6ef18

17 files changed

+559
-45
lines changed

sponsors/admin.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.template import Context, Template
77
from django.contrib import admin
88
from django.contrib.humanize.templatetags.humanize import intcomma
9+
from django.forms import ModelForm
910
from django.urls import path, reverse, resolve
1011
from django.utils.functional import cached_property
1112
from django.utils.html import mark_safe
@@ -46,7 +47,14 @@ class SponsorshipProgramAdmin(OrderedModelAdmin):
4647
]
4748

4849

50+
class MultiPartForceForm(ModelForm):
51+
def is_multipart(self):
52+
return True
53+
54+
4955
class BenefitFeatureConfigurationInline(StackedPolymorphicInline):
56+
form = MultiPartForceForm
57+
5058
class LogoPlacementConfigurationInline(StackedPolymorphicInline.Child):
5159
model = LogoPlacementConfiguration
5260

@@ -60,23 +68,38 @@ class EmailTargetableConfigurationInline(StackedPolymorphicInline.Child):
6068
def display(self, obj):
6169
return "Enabled"
6270

63-
class RequiredImgAssetConfigurationInline(StackedPolymorphicInline.Child):
71+
class BaseAssetInline(StackedPolymorphicInline.Child):
72+
73+
def get_readonly_fields(self, request, obj=None):
74+
fields = list(super().get_readonly_fields(request, obj))
75+
if obj:
76+
fields.extend(["internal_name", "related_to"])
77+
return fields
78+
79+
class RequiredImgAssetConfigurationInline(BaseAssetInline):
6480
model = RequiredImgAssetConfiguration
6581
form = RequiredImgAssetConfigurationForm
6682

67-
class RequiredTextAssetConfigurationInline(StackedPolymorphicInline.Child):
83+
class RequiredTextAssetConfigurationInline(BaseAssetInline):
6884
model = RequiredTextAssetConfiguration
6985

86+
class ProvidedTextAssetConfigurationInline(BaseAssetInline):
87+
model = ProvidedTextAssetConfiguration
88+
89+
class ProvidedFileAssetConfigurationInline(BaseAssetInline):
90+
model = ProvidedFileAssetConfiguration
91+
7092
model = BenefitFeatureConfiguration
7193
child_inlines = [
7294
LogoPlacementConfigurationInline,
7395
TieredQuantityConfigurationInline,
7496
EmailTargetableConfigurationInline,
7597
RequiredImgAssetConfigurationInline,
7698
RequiredTextAssetConfigurationInline,
99+
ProvidedTextAssetConfigurationInline,
100+
ProvidedFileAssetConfigurationInline,
77101
]
78102

79-
80103
@admin.register(SponsorshipBenefit)
81104
class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin):
82105
change_form_template = "sponsors/admin/sponsorshipbenefit_change_form.html"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.24 on 2022-01-10 18:41
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('sponsors', '0067_sponsorbenefit_a_la_carte'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='sponsorship',
15+
name='for_modified_package',
16+
field=models.BooleanField(default=False, help_text="If true, it means the user customized the package's benefits. Changes are listed under section 'User Customizations'."),
17+
),
18+
]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Generated by Django 2.2.24 on 2022-01-10 21:48
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', '0068_auto_20220110_1841'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='ProvidedTextAsset',
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 text 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+
],
24+
options={
25+
'verbose_name': 'Provided Text',
26+
'verbose_name_plural': 'Provided Texts',
27+
'abstract': False,
28+
'base_manager_name': 'objects',
29+
},
30+
bases=(sponsors.models.benefits.ProvidedAssetMixin, 'sponsors.benefitfeature', models.Model),
31+
),
32+
migrations.CreateModel(
33+
name='ProvidedTextAssetConfiguration',
34+
fields=[
35+
('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')),
36+
('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')),
37+
('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')),
38+
('label', models.CharField(help_text="What's the title used to display the text input to the sponsor?", max_length=256)),
39+
('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the input should be populated', max_length=256)),
40+
],
41+
options={
42+
'verbose_name': 'Provided Text Configuration',
43+
'verbose_name_plural': 'Provided Text Configurations',
44+
'abstract': False,
45+
'base_manager_name': 'objects',
46+
},
47+
bases=(sponsors.models.benefits.AssetConfigurationMixin, 'sponsors.benefitfeatureconfiguration', models.Model),
48+
),
49+
migrations.AddConstraint(
50+
model_name='providedtextassetconfiguration',
51+
constraint=models.UniqueConstraint(fields=('internal_name',), name='uniq_provided_text_asset_cfg'),
52+
),
53+
]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Generated by Django 2.2.24 on 2022-01-11 20:55
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import sponsors.models.assets
6+
import sponsors.models.benefits
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('sponsors', '0069_auto_20220110_2148'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='FileAsset',
18+
fields=[
19+
('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')),
20+
('file', models.FileField(null=True, upload_to=sponsors.models.assets.generic_asset_path)),
21+
],
22+
options={
23+
'verbose_name': 'File Asset',
24+
'verbose_name_plural': 'File Assets',
25+
},
26+
bases=('sponsors.genericasset',),
27+
),
28+
migrations.CreateModel(
29+
name='ProvidedFileAsset',
30+
fields=[
31+
('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')),
32+
('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')),
33+
('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')),
34+
('shared', models.BooleanField(default=False)),
35+
('label', models.CharField(help_text="What's the title used to display the file to the sponsor?", max_length=256)),
36+
('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the file should be used', max_length=256)),
37+
('shared_file', models.FileField(blank=True, null=True, upload_to='')),
38+
],
39+
options={
40+
'verbose_name': 'Provided File',
41+
'verbose_name_plural': 'Provided Files',
42+
'abstract': False,
43+
'base_manager_name': 'objects',
44+
},
45+
bases=(sponsors.models.benefits.ProvidedAssetMixin, 'sponsors.benefitfeature', models.Model),
46+
),
47+
migrations.CreateModel(
48+
name='ProvidedFileAssetConfiguration',
49+
fields=[
50+
('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')),
51+
('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')),
52+
('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')),
53+
('shared', models.BooleanField(default=False)),
54+
('label', models.CharField(help_text="What's the title used to display the file to the sponsor?", max_length=256)),
55+
('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the file should be used', max_length=256)),
56+
('shared_file', models.FileField(blank=True, null=True, upload_to='')),
57+
],
58+
options={
59+
'verbose_name': 'Provided File Configuration',
60+
'verbose_name_plural': 'Provided File Configurations',
61+
'abstract': False,
62+
'base_manager_name': 'objects',
63+
},
64+
bases=(sponsors.models.benefits.AssetConfigurationMixin, 'sponsors.benefitfeatureconfiguration', models.Model),
65+
),
66+
migrations.AddField(
67+
model_name='providedtextasset',
68+
name='shared',
69+
field=models.BooleanField(default=False),
70+
),
71+
migrations.AddField(
72+
model_name='providedtextassetconfiguration',
73+
name='shared',
74+
field=models.BooleanField(default=False),
75+
),
76+
migrations.AddConstraint(
77+
model_name='providedfileassetconfiguration',
78+
constraint=models.UniqueConstraint(fields=('internal_name',), name='uniq_provided_file_asset_cfg'),
79+
),
80+
]

sponsors/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .benefits import BaseLogoPlacement, BaseTieredQuantity, BaseEmailTargetable, BenefitFeatureConfiguration, \
1111
LogoPlacementConfiguration, TieredQuantityConfiguration, EmailTargetableConfiguration, BenefitFeature, \
1212
LogoPlacement, EmailTargetable, TieredQuantity, RequiredImgAsset, RequiredImgAssetConfiguration, \
13-
RequiredTextAssetConfiguration, RequiredTextAsset
13+
RequiredTextAssetConfiguration, RequiredTextAsset, ProvidedTextAssetConfiguration, ProvidedTextAsset, \
14+
ProvidedFileAssetConfiguration, ProvidedFileAsset
1415
from .sponsorship import Sponsorship, SponsorshipProgram, SponsorshipBenefit, Sponsorship, SponsorshipPackage
1516
from .contract import LegalClause, Contract, signed_contract_random_path

sponsors/models/assets.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,26 @@ def value(self):
9090
@value.setter
9191
def value(self, value):
9292
self.text = value
93+
94+
95+
class FileAsset(GenericAsset):
96+
file = models.FileField(
97+
upload_to=generic_asset_path,
98+
blank=False,
99+
null=True,
100+
)
101+
102+
def __str__(self):
103+
return f"File asset: {self.internal_name}"
104+
105+
class Meta:
106+
verbose_name = "File Asset"
107+
verbose_name_plural = "File Assets"
108+
109+
@property
110+
def value(self):
111+
return self.file
112+
113+
@value.setter
114+
def value(self, value):
115+
self.file = value

0 commit comments

Comments
 (0)