diff --git a/sponsors/admin.py b/sponsors/admin.py index c2541ab21..d15cf2882 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -6,6 +6,7 @@ from django.template import Context, Template from django.contrib import admin from django.contrib.humanize.templatetags.humanize import intcomma +from django.forms import ModelForm from django.urls import path, reverse, resolve from django.utils.functional import cached_property from django.utils.html import mark_safe @@ -46,7 +47,14 @@ class SponsorshipProgramAdmin(OrderedModelAdmin): ] +class MultiPartForceForm(ModelForm): + def is_multipart(self): + return True + + class BenefitFeatureConfigurationInline(StackedPolymorphicInline): + form = MultiPartForceForm + class LogoPlacementConfigurationInline(StackedPolymorphicInline.Child): model = LogoPlacementConfiguration @@ -60,13 +68,27 @@ class EmailTargetableConfigurationInline(StackedPolymorphicInline.Child): def display(self, obj): return "Enabled" - class RequiredImgAssetConfigurationInline(StackedPolymorphicInline.Child): + class BaseAssetInline(StackedPolymorphicInline.Child): + + def get_readonly_fields(self, request, obj=None): + fields = list(super().get_readonly_fields(request, obj)) + if obj: + fields.extend(["internal_name", "related_to"]) + return fields + + class RequiredImgAssetConfigurationInline(BaseAssetInline): model = RequiredImgAssetConfiguration form = RequiredImgAssetConfigurationForm - class RequiredTextAssetConfigurationInline(StackedPolymorphicInline.Child): + class RequiredTextAssetConfigurationInline(BaseAssetInline): model = RequiredTextAssetConfiguration + class ProvidedTextAssetConfigurationInline(BaseAssetInline): + model = ProvidedTextAssetConfiguration + + class ProvidedFileAssetConfigurationInline(BaseAssetInline): + model = ProvidedFileAssetConfiguration + model = BenefitFeatureConfiguration child_inlines = [ LogoPlacementConfigurationInline, @@ -74,9 +96,10 @@ class RequiredTextAssetConfigurationInline(StackedPolymorphicInline.Child): EmailTargetableConfigurationInline, RequiredImgAssetConfigurationInline, RequiredTextAssetConfigurationInline, + ProvidedTextAssetConfigurationInline, + ProvidedFileAssetConfigurationInline, ] - @admin.register(SponsorshipBenefit) class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin): change_form_template = "sponsors/admin/sponsorshipbenefit_change_form.html" diff --git a/sponsors/migrations/0068_auto_20220110_1841.py b/sponsors/migrations/0068_auto_20220110_1841.py new file mode 100644 index 000000000..8149d57da --- /dev/null +++ b/sponsors/migrations/0068_auto_20220110_1841.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.24 on 2022-01-10 18:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0067_sponsorbenefit_a_la_carte'), + ] + + operations = [ + migrations.AlterField( + model_name='sponsorship', + name='for_modified_package', + 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'."), + ), + ] diff --git a/sponsors/migrations/0069_auto_20220110_2148.py b/sponsors/migrations/0069_auto_20220110_2148.py new file mode 100644 index 000000000..8492ce06b --- /dev/null +++ b/sponsors/migrations/0069_auto_20220110_2148.py @@ -0,0 +1,53 @@ +# Generated by Django 2.2.24 on 2022-01-10 21:48 + +from django.db import migrations, models +import django.db.models.deletion +import sponsors.models.benefits + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0068_auto_20220110_1841'), + ] + + operations = [ + migrations.CreateModel( + name='ProvidedTextAsset', + fields=[ + ('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')), + ('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')), + ('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')), + ('label', models.CharField(help_text="What's the title used to display the text input to the sponsor?", max_length=256)), + ('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the input should be populated', max_length=256)), + ], + options={ + 'verbose_name': 'Provided Text', + 'verbose_name_plural': 'Provided Texts', + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=(sponsors.models.benefits.ProvidedAssetMixin, 'sponsors.benefitfeature', models.Model), + ), + migrations.CreateModel( + name='ProvidedTextAssetConfiguration', + fields=[ + ('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')), + ('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')), + ('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')), + ('label', models.CharField(help_text="What's the title used to display the text input to the sponsor?", max_length=256)), + ('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the input should be populated', max_length=256)), + ], + options={ + 'verbose_name': 'Provided Text Configuration', + 'verbose_name_plural': 'Provided Text Configurations', + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=(sponsors.models.benefits.AssetConfigurationMixin, 'sponsors.benefitfeatureconfiguration', models.Model), + ), + migrations.AddConstraint( + model_name='providedtextassetconfiguration', + constraint=models.UniqueConstraint(fields=('internal_name',), name='uniq_provided_text_asset_cfg'), + ), + ] diff --git a/sponsors/migrations/0070_auto_20220111_2055.py b/sponsors/migrations/0070_auto_20220111_2055.py new file mode 100644 index 000000000..94f8075cb --- /dev/null +++ b/sponsors/migrations/0070_auto_20220111_2055.py @@ -0,0 +1,80 @@ +# Generated by Django 2.2.24 on 2022-01-11 20:55 + +from django.db import migrations, models +import django.db.models.deletion +import sponsors.models.assets +import sponsors.models.benefits + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0069_auto_20220110_2148'), + ] + + operations = [ + migrations.CreateModel( + name='FileAsset', + fields=[ + ('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')), + ('file', models.FileField(null=True, upload_to=sponsors.models.assets.generic_asset_path)), + ], + options={ + 'verbose_name': 'File Asset', + 'verbose_name_plural': 'File Assets', + }, + bases=('sponsors.genericasset',), + ), + migrations.CreateModel( + name='ProvidedFileAsset', + fields=[ + ('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')), + ('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')), + ('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')), + ('shared', models.BooleanField(default=False)), + ('label', models.CharField(help_text="What's the title used to display the file to the sponsor?", max_length=256)), + ('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the file should be used', max_length=256)), + ('shared_file', models.FileField(blank=True, null=True, upload_to='')), + ], + options={ + 'verbose_name': 'Provided File', + 'verbose_name_plural': 'Provided Files', + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=(sponsors.models.benefits.ProvidedAssetMixin, 'sponsors.benefitfeature', models.Model), + ), + migrations.CreateModel( + name='ProvidedFileAssetConfiguration', + fields=[ + ('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')), + ('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')), + ('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')), + ('shared', models.BooleanField(default=False)), + ('label', models.CharField(help_text="What's the title used to display the file to the sponsor?", max_length=256)), + ('help_text', models.CharField(blank=True, default='', help_text='Any helper comment on how the file should be used', max_length=256)), + ('shared_file', models.FileField(blank=True, null=True, upload_to='')), + ], + options={ + 'verbose_name': 'Provided File Configuration', + 'verbose_name_plural': 'Provided File Configurations', + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=(sponsors.models.benefits.AssetConfigurationMixin, 'sponsors.benefitfeatureconfiguration', models.Model), + ), + migrations.AddField( + model_name='providedtextasset', + name='shared', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='providedtextassetconfiguration', + name='shared', + field=models.BooleanField(default=False), + ), + migrations.AddConstraint( + model_name='providedfileassetconfiguration', + constraint=models.UniqueConstraint(fields=('internal_name',), name='uniq_provided_file_asset_cfg'), + ), + ] diff --git a/sponsors/models/__init__.py b/sponsors/models/__init__.py index d7d2759be..143bb83fa 100644 --- a/sponsors/models/__init__.py +++ b/sponsors/models/__init__.py @@ -10,6 +10,7 @@ from .benefits import BaseLogoPlacement, BaseTieredQuantity, BaseEmailTargetable, BenefitFeatureConfiguration, \ LogoPlacementConfiguration, TieredQuantityConfiguration, EmailTargetableConfiguration, BenefitFeature, \ LogoPlacement, EmailTargetable, TieredQuantity, RequiredImgAsset, RequiredImgAssetConfiguration, \ - RequiredTextAssetConfiguration, RequiredTextAsset + RequiredTextAssetConfiguration, RequiredTextAsset, ProvidedTextAssetConfiguration, ProvidedTextAsset, \ + ProvidedFileAssetConfiguration, ProvidedFileAsset from .sponsorship import Sponsorship, SponsorshipProgram, SponsorshipBenefit, Sponsorship, SponsorshipPackage from .contract import LegalClause, Contract, signed_contract_random_path diff --git a/sponsors/models/assets.py b/sponsors/models/assets.py index 35ba198fa..3268d2594 100644 --- a/sponsors/models/assets.py +++ b/sponsors/models/assets.py @@ -90,3 +90,26 @@ def value(self): @value.setter def value(self, value): self.text = value + + +class FileAsset(GenericAsset): + file = models.FileField( + upload_to=generic_asset_path, + blank=False, + null=True, + ) + + def __str__(self): + return f"File asset: {self.internal_name}" + + class Meta: + verbose_name = "File Asset" + verbose_name_plural = "File Assets" + + @property + def value(self): + return self.file + + @value.setter + def value(self, value): + self.file = value diff --git a/sponsors/models/benefits.py b/sponsors/models/benefits.py index 0051a84c0..c99b1e317 100644 --- a/sponsors/models/benefits.py +++ b/sponsors/models/benefits.py @@ -7,7 +7,7 @@ from django.urls import reverse from polymorphic.models import PolymorphicModel -from sponsors.models.assets import ImgAsset, TextAsset +from sponsors.models.assets import ImgAsset, TextAsset, FileAsset from sponsors.models.enums import PublisherChoices, LogoPlacementChoices, AssetsRelatedTo ######################################## @@ -56,7 +56,7 @@ class Meta: abstract = True -class BaseRequiredAsset(models.Model): +class BaseAsset(models.Model): ASSET_CLASS = None related_to = models.CharField( @@ -87,7 +87,24 @@ class Meta: abstract = True -class RequiredAssetConfigurationMixin: +class BaseRequiredAsset(BaseAsset): + class Meta: + abstract = True + + +class BaseProvidedAsset(BaseAsset): + shared = models.BooleanField( + default = False, + ) + + def shared_value(self): + return None + + class Meta: + abstract = True + + +class AssetConfigurationMixin: """ This class should be used to implement assets configuration. It's a mixin to updates the benefit feature creation to also @@ -97,7 +114,7 @@ class RequiredAssetConfigurationMixin: def create_benefit_feature(self, sponsor_benefit, **kwargs): if not self.ASSET_CLASS: raise NotImplementedError( - "Subclasses of RequiredAssetConfigurationMixin must define an ASSET_CLASS attribute.") + "Subclasses of AssetConfigurationMixin must define an ASSET_CLASS attribute.") # Super: BenefitFeatureConfiguration.create_benefit_feature benefit_feature = super().create_benefit_feature(sponsor_benefit, **kwargs) @@ -149,14 +166,54 @@ class Meta(BaseRequiredAsset.Meta): abstract = True -class RequiredAssetMixin: - """ - This class should be used to implement required assets. - It's a mixin to get the information submitted by the user - and which is stored in the related asset class. - """ +class BaseProvidedTextAsset(BaseProvidedAsset): + ASSET_CLASS = TextAsset + + label = models.CharField( + max_length=256, + help_text="What's the title used to display the text input to the sponsor?" + ) + help_text = models.CharField( + max_length=256, + help_text="Any helper comment on how the input should be populated", + default="", + blank=True + ) + + class Meta(BaseProvidedAsset.Meta): + abstract = True + +class BaseProvidedFileAsset(BaseProvidedAsset): + ASSET_CLASS = FileAsset + + label = models.CharField( + max_length=256, + help_text="What's the title used to display the file to the sponsor?" + ) + help_text = models.CharField( + max_length=256, + help_text="Any helper comment on how the file should be used", + default="", + blank=True + ) + shared_file = models.FileField(blank=True, null=True) + + def shared_value(self): + return self.shared_file + + class Meta(BaseProvidedAsset.Meta): + abstract = True + + +class AssetMixin: def __related_asset(self): + """ + This method exists to avoid FK relationships between the GenericAsset + and reuired asset objects. This is to decouple the assets set up from the + real assets value in a way that, if the first gets deleted, the second can + still be re used. + """ object = self.sponsor_benefit.sponsorship if self.related_to == AssetsRelatedTo.SPONSOR.value: object = self.sponsor_benefit.sponsorship.sponsor @@ -180,6 +237,32 @@ def user_edit_url(self): return url + f"?required_asset={self.pk}" + @property + def user_view_url(self): + url = reverse("users:view_provided_sponsorship_assets", args=[self.sponsor_benefit.sponsorship.pk]) + return url + f"?provided_asset={self.pk}" + +class RequiredAssetMixin(AssetMixin): + """ + This class should be used to implement required assets. + It's a mixin to get the information submitted by the user + and which is stored in the related asset class. + """ + pass + +class ProvidedAssetMixin(AssetMixin): + """ + This class should be used to implement provided assets. + It's a mixin to get the information submitted by the staff + and which is stored in the related asset class. + """ + + @property + def value(self): + if hasattr(self, 'shared') and self.shared: + return self.shared_value() + return super().value + ###################################################### # SponsorshipBenefit features configuration models class BenefitFeatureConfiguration(PolymorphicModel): @@ -303,7 +386,7 @@ def __str__(self): return f"Email targeatable configuration" -class RequiredImgAssetConfiguration(RequiredAssetConfigurationMixin, BaseRequiredImgAsset, BenefitFeatureConfiguration): +class RequiredImgAssetConfiguration(AssetConfigurationMixin, BaseRequiredImgAsset, BenefitFeatureConfiguration): class Meta(BaseRequiredImgAsset.Meta, BenefitFeatureConfiguration.Meta): verbose_name = "Require Image Configuration" verbose_name_plural = "Require Image Configurations" @@ -317,7 +400,7 @@ def benefit_feature_class(self): return RequiredImgAsset -class RequiredTextAssetConfiguration(RequiredAssetConfigurationMixin, BaseRequiredTextAsset, +class RequiredTextAssetConfiguration(AssetConfigurationMixin, BaseRequiredTextAsset, BenefitFeatureConfiguration): class Meta(BaseRequiredTextAsset.Meta, BenefitFeatureConfiguration.Meta): verbose_name = "Require Text Configuration" @@ -332,6 +415,36 @@ def benefit_feature_class(self): return RequiredTextAsset +class ProvidedTextAssetConfiguration(AssetConfigurationMixin, BaseProvidedTextAsset, + BenefitFeatureConfiguration): + class Meta(BaseProvidedTextAsset.Meta, BenefitFeatureConfiguration.Meta): + verbose_name = "Provided Text Configuration" + verbose_name_plural = "Provided Text Configurations" + constraints = [UniqueConstraint(fields=["internal_name"], name="uniq_provided_text_asset_cfg")] + + def __str__(self): + return f"Provided text configuration" + + @property + def benefit_feature_class(self): + return ProvidedTextAsset + + +class ProvidedFileAssetConfiguration(AssetConfigurationMixin, BaseProvidedFileAsset, + BenefitFeatureConfiguration): + class Meta(BaseProvidedFileAsset.Meta, BenefitFeatureConfiguration.Meta): + verbose_name = "Provided File Configuration" + verbose_name_plural = "Provided File Configurations" + constraints = [UniqueConstraint(fields=["internal_name"], name="uniq_provided_file_asset_cfg")] + + def __str__(self): + return f"Provided File configuration" + + @property + def benefit_feature_class(self): + return ProvidedFileAsset + + #################################### # SponsorBenefit features models class BenefitFeature(PolymorphicModel): @@ -420,3 +533,21 @@ def as_form_field(self, **kwargs): 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) + + +class ProvidedTextAsset(ProvidedAssetMixin, BaseProvidedTextAsset, BenefitFeature): + class Meta(BaseProvidedTextAsset.Meta, BenefitFeature.Meta): + verbose_name = "Provided Text" + verbose_name_plural = "Provided Texts" + + def __str__(self): + return f"Provided text" + + +class ProvidedFileAsset(ProvidedAssetMixin, BaseProvidedFileAsset, BenefitFeature): + class Meta(BaseProvidedFileAsset.Meta, BenefitFeature.Meta): + verbose_name = "Provided File" + verbose_name_plural = "Provided Files" + + def __str__(self): + return f"Provided file" diff --git a/sponsors/models/managers.py b/sponsors/models/managers.py index 13716db49..cc8657aea 100644 --- a/sponsors/models/managers.py +++ b/sponsors/models/managers.py @@ -100,6 +100,12 @@ def list_advertisables(self): class BenefitFeatureQuerySet(PolymorphicQuerySet): + def delete(self): + if not self.polymorphic_disabled: + return self.non_polymorphic().delete() + else: + return super().delete() + def from_sponsorship(self, sponsorship): return self.filter(sponsor_benefit__sponsorship=sponsorship).select_related("sponsor_benefit__sponsorship") @@ -107,3 +113,8 @@ def required_assets(self): from sponsors.models.benefits import RequiredAssetMixin required_assets_classes = RequiredAssetMixin.__subclasses__() return self.instance_of(*required_assets_classes).select_related("sponsor_benefit__sponsorship") + + def provided_assets(self): + from sponsors.models.benefits import ProvidedAssetMixin + provided_assets_classes = ProvidedAssetMixin.__subclasses__() + return self.instance_of(*provided_assets_classes).select_related("sponsor_benefit__sponsorship") diff --git a/sponsors/models/sponsors.py b/sponsors/models/sponsors.py index 8cd7f26e9..7475281a7 100644 --- a/sponsors/models/sponsors.py +++ b/sponsors/models/sponsors.py @@ -4,6 +4,7 @@ from allauth.account.models import EmailAddress from django.conf import settings from django.db import models +from django.core.exceptions import ObjectDoesNotExist from django_countries.fields import CountryField from ordered_model.models import OrderedModel from django.contrib.contenttypes.fields import GenericRelation @@ -252,5 +253,30 @@ def name_for_display(self): name = feature.display_modifier(name) return name + def reset_attributes(self, benefit): + """ + This method resets all the sponsor benefit information + fetching new data from the sponsorship benefit. + """ + self.program_name = benefit.program.name + self.name = benefit.name + self.description = benefit.description + self.program = benefit.program + self.benefit_internal_value = benefit.internal_value + self.a_la_carte = benefit.a_la_carte + self.added_by_user = self.added_by_user or self.a_la_carte + + # generate benefit features from benefit features configurations + features = self.features.all().delete() + for feature_config in benefit.features_config.all(): + feature_config.create_benefit_feature(self) + + self.save() + + + def delete(self): + self.features.all().delete() + super().delete() + class Meta(OrderedModel.Meta): pass diff --git a/sponsors/tests/test_models.py b/sponsors/tests/test_models.py index a73af52ec..0822a9efa 100644 --- a/sponsors/tests/test_models.py +++ b/sponsors/tests/test_models.py @@ -636,6 +636,87 @@ def test_sponsor_benefit_from_a_la_carte_one(self): self.assertTrue(sponsor_benefit.added_by_user) self.assertTrue(sponsor_benefit.a_la_carte) + def test_reset_attributes_updates_all_basic_information(self): + benefit = baker.make( + SponsorBenefit, sponsorship_benefit=self.sponsorship_benefit + ) + # both have different random values + self.assertNotEqual(benefit.name, self.sponsorship_benefit.name) + + benefit.reset_attributes(self.sponsorship_benefit) + benefit.refresh_from_db() + + self.assertEqual(benefit.name, self.sponsorship_benefit.name) + self.assertEqual(benefit.description, self.sponsorship_benefit.description) + self.assertEqual(benefit.program_name, self.sponsorship_benefit.program.name) + self.assertEqual(benefit.program, self.sponsorship_benefit.program) + self.assertEqual(benefit.benefit_internal_value, self.sponsorship_benefit.internal_value) + self.assertEqual(benefit.a_la_carte, self.sponsorship_benefit.a_la_carte) + + def test_reset_attributes_add_new_features(self): + RequiredTextAssetConfiguration.objects.create( + benefit=self.sponsorship_benefit, + related_to="sponsorship", + internal_name="foo", + label="Text", + ) + benefit = baker.make( + SponsorBenefit, sponsorship_benefit=self.sponsorship_benefit + ) + # no previous feature + self.assertFalse(benefit.features.count()) + + benefit.reset_attributes(self.sponsorship_benefit) + benefit.refresh_from_db() + + self.assertEqual(1, benefit.features.count()) + + def test_reset_attributes_delete_removed_features(self): + cfg = RequiredTextAssetConfiguration.objects.create( + benefit=self.sponsorship_benefit, + related_to="sponsorship", + internal_name="foo", + label="Text", + ) + benefit = SponsorBenefit.new_copy( + self.sponsorship_benefit, sponsorship=self.sponsorship + ) + self.assertEqual(1, benefit.features.count()) + cfg.delete() + + benefit.reset_attributes(self.sponsorship_benefit) + benefit.refresh_from_db() + + # no previous feature + self.assertFalse(benefit.features.count()) + + def test_reset_attributes_recreate_features_but_keeping_previous_values(self): + cfg = RequiredTextAssetConfiguration.objects.create( + benefit=self.sponsorship_benefit, + related_to="sponsorship", + internal_name="foo", + label="Text", + ) + benefit = SponsorBenefit.new_copy( + self.sponsorship_benefit, sponsorship=self.sponsorship + ) + + feature = RequiredTextAsset.objects.get() + feature.value = "foo" + feature.save() + cfg.label = "New text" + cfg.save() + + benefit.reset_attributes(self.sponsorship_benefit) + benefit.refresh_from_db() + + # no previous feature + self.assertEqual(1, benefit.features.count()) + asset = benefit.features.required_assets().get() + self.assertEqual(asset.label, "New text") + self.assertEqual(asset.value, "foo") + + ########### # Email notification tests class SponsorEmailNotificationTemplateTests(TestCase): diff --git a/sponsors/tests/test_views_admin.py b/sponsors/tests/test_views_admin.py index 0de2bdc75..1e4ba15d6 100644 --- a/sponsors/tests/test_views_admin.py +++ b/sponsors/tests/test_views_admin.py @@ -833,16 +833,10 @@ def test_update_selected_sponsorships_only(self): response = self.client.post(self.url, data=self.data) - # delete existing sponsor benefit - self.assertFalse(SponsorBenefit.objects.filter(id=self.sponsor_benefit.id).exists()) - # make sure a new one was created - new_sponsor_benefit = SponsorBenefit.objects.get( - sponsorship=self.sponsor_benefit.sponsorship, - sponsorship_benefit=self.benefit, - ) - self.assertEqual(new_sponsor_benefit.name, "New name") - self.assertEqual(new_sponsor_benefit.description, "New description") - self.assertTrue(new_sponsor_benefit.added_by_user) + self.sponsor_benefit.refresh_from_db() + self.assertEqual(self.sponsor_benefit.name, "New name") + self.assertEqual(self.sponsor_benefit.description, "New description") + self.assertTrue(self.sponsor_benefit.added_by_user) # make sure sponsor benefit from unselected sponsorships wasn't deleted other_sponsor_benefit.refresh_from_db() self.assertEqual(other_sponsor_benefit.name, prev_name) diff --git a/sponsors/views_admin.py b/sponsors/views_admin.py index b1a216abd..c2d6a11ca 100644 --- a/sponsors/views_admin.py +++ b/sponsors/views_admin.py @@ -240,7 +240,8 @@ def update_related_sponsorships(ModelAdmin, request, pk): Given a SponsorshipBeneefit, update all releated SponsorBenefit from the Sponsorship listed in the post payload """ - benefit = get_object_or_404(ModelAdmin.get_queryset(request), pk=pk) + qs = ModelAdmin.get_queryset(request).select_related("program") + benefit = get_object_or_404(qs, pk=pk) initial = {"sponsorships": [sp.pk for sp in benefit.related_sponsorships]} form = SponsorshipsListForm.with_benefit(benefit, initial=initial) @@ -252,14 +253,7 @@ def update_related_sponsorships(ModelAdmin, request, pk): related_benefits = benefit.sponsorbenefit_set.all() for sp in sponsorships: sponsor_benefit = related_benefits.get(sponsorship=sp) - sponsor_benefit.delete() - - # recreate sponsor benefit considering updated benefit/feature configs - SponsorBenefit.new_copy( - benefit, - sponsorship=sp, - added_by_user=sponsor_benefit.added_by_user - ) + sponsor_benefit.reset_attributes(benefit) ModelAdmin.message_user( request, f"{len(sponsorships)} related sponsorships updated!", messages.SUCCESS diff --git a/templates/users/sponsorship_assets_view.html b/templates/users/sponsorship_assets_view.html new file mode 100644 index 000000000..3bb0d2d37 --- /dev/null +++ b/templates/users/sponsorship_assets_view.html @@ -0,0 +1,31 @@ +{% extends "users/base.html" %} +{% load widget_tweaks %} +{% load humanize pipeline %} + +{% block head %} + {% stylesheet 'font-awesome' %} +{% endblock %} + +{% block page_title %} + {{ sponsorship }} assets | {{ SITE_INFO.site_name }} +{% endblock %} + +{% block body_attributes %}class="psf signup default-page"{% endblock %} + +{% block main-nav_attributes %}psf-navigation{% endblock %} + +{% block user_content %} +
+

View assets for {{ sponsorship.sponsor }} sponsorship

+ + {% for asset in provided_assets %} +

{{ asset.sponsor_benefit }} benefit provides you with {{ asset.label }}:

+ {% if asset.polymorphic_ctype.name == "Provided Text" %} +
{{ asset.value }}
+ {% else %} + {{ asset.value }} + {% endif %} + {% endfor %} + +
+{% endblock %} diff --git a/templates/users/sponsorship_detail.html b/templates/users/sponsorship_detail.html index 7e83d3789..6c9bceb13 100644 --- a/templates/users/sponsorship_detail.html +++ b/templates/users/sponsorship_detail.html @@ -87,13 +87,16 @@

Application Data

- {% if assets or fulfilled_assets %} + + +
+ {% if required_assets or fulfilled_assets %}

Required Assets

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


{% endif %} -
-
+ {% if provided_assets %} +
+

Provided Assets

+

Assets from the PSF related to your sponsorship.

+
+ +
+ Or you can also click here to view all the assets under the same page. +
+ {% endif %} +

Sponsorship Benefits