diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml index d6073540..019e13b7 100644 --- a/.annotation_safe_list.yml +++ b/.annotation_safe_list.yml @@ -57,3 +57,5 @@ waffle.Switch: ".. no_pii:": "This model has no PII" subscriptions.HistoricalSubscriptionLicenseSource: ".. no_pii:": "This model has no PII" +subscriptions.HistoricalCustomSubscriptionExpirationMessaging: + ".. no_pii:": "This model has no PII" diff --git a/license_manager/apps/api/serializers.py b/license_manager/apps/api/serializers.py index c6795d21..9a5735a7 100644 --- a/license_manager/apps/api/serializers.py +++ b/license_manager/apps/api/serializers.py @@ -342,6 +342,11 @@ class MinimalCustomerAgreementSerializer(serializers.ModelSerializer): """ subscription_for_auto_applied_licenses = serializers.SerializerMethodField() + has_custom_license_expiration_messaging_v2 = serializers.SerializerMethodField() + modal_header_text_v2 = serializers.SerializerMethodField() + expired_subscription_modal_messaging_v2 = serializers.SerializerMethodField() + button_label_in_modal_v2 = serializers.SerializerMethodField() + url_for_button_in_modal_v2 = serializers.SerializerMethodField() class Meta: model = CustomerAgreement @@ -355,17 +360,47 @@ class Meta: 'subscription_for_auto_applied_licenses', 'available_subscription_catalogs', 'enable_auto_applied_subscriptions_with_universal_link', - 'has_custom_license_expiration_messaging', - 'modal_header_text', - 'expired_subscription_modal_messaging', - 'button_label_in_modal', - 'url_for_button_in_modal', + 'has_custom_license_expiration_messaging_v2', + 'modal_header_text_v2', + 'expired_subscription_modal_messaging_v2', + 'button_label_in_modal_v2', + 'url_for_button_in_modal_v2', ] def get_subscription_for_auto_applied_licenses(self, obj): subscription_plan = obj.auto_applicable_subscription return subscription_plan.uuid if subscription_plan else None + def get_has_custom_license_expiration_messaging_v2(self, obj): + custom_subscription_expiration_messaging = obj.custom_subscription_expiration_messaging + if custom_subscription_expiration_messaging: + return custom_subscription_expiration_messaging.has_custom_license_expiration_messaging + return None + + def get_modal_header_text_v2(self, obj): + custom_subscription_expiration_messaging = obj.custom_subscription_expiration_messaging + if custom_subscription_expiration_messaging: + return custom_subscription_expiration_messaging.modal_header_text + return None + + def get_expired_subscription_modal_messaging_v2(self, obj): + custom_subscription_expiration_messaging = obj.custom_subscription_expiration_messaging + if custom_subscription_expiration_messaging: + return custom_subscription_expiration_messaging.expired_subscription_modal_messaging + return None + + def get_button_label_in_modal_v2(self, obj): + custom_subscription_expiration_messaging = obj.custom_subscription_expiration_messaging + if custom_subscription_expiration_messaging: + return custom_subscription_expiration_messaging.button_label_in_modal + return None + + def get_url_for_button_in_modal_v2(self, obj): + custom_subscription_expiration_messaging = obj.custom_subscription_expiration_messaging + if custom_subscription_expiration_messaging: + return custom_subscription_expiration_messaging.url_for_button_in_modal + return None + class CustomerAgreementSerializer(MinimalCustomerAgreementSerializer): """ diff --git a/license_manager/apps/subscriptions/admin.py b/license_manager/apps/subscriptions/admin.py index 17d64662..8209d3dd 100644 --- a/license_manager/apps/subscriptions/admin.py +++ b/license_manager/apps/subscriptions/admin.py @@ -27,6 +27,7 @@ ) from license_manager.apps.subscriptions.models import ( CustomerAgreement, + CustomSubscriptionExpirationMessaging, License, LicenseEvent, LicenseTransferJob, @@ -402,6 +403,14 @@ def save_model(self, request, obj, form, change): obj.provision_licenses() +@admin.register(CustomSubscriptionExpirationMessaging) +class CustomSubscriptionExpirationMessagingAdmin(DjangoQLSearchMixin, admin.ModelAdmin): + list_display = ( + 'customer_agreement', + 'has_custom_license_expiration_messaging', + ) + + @admin.register(CustomerAgreement) class CustomerAgreementAdmin(admin.ModelAdmin): form = CustomerAgreementAdminForm @@ -418,11 +427,6 @@ class CustomerAgreementAdmin(admin.ModelAdmin): 'license_duration_before_purge', 'disable_onboarding_notifications', 'enable_auto_applied_subscriptions_with_universal_link', - 'has_custom_license_expiration_messaging', - 'modal_header_text', - 'expired_subscription_modal_messaging', - 'button_label_in_modal', - 'url_for_button_in_modal', ) custom_fields = ('subscription_for_auto_applied_licenses',) diff --git a/license_manager/apps/subscriptions/migrations/0074_historicalcustomsubscriptionexpirationmessaging_and_more.py b/license_manager/apps/subscriptions/migrations/0074_historicalcustomsubscriptionexpirationmessaging_and_more.py new file mode 100644 index 00000000..2941eae8 --- /dev/null +++ b/license_manager/apps/subscriptions/migrations/0074_historicalcustomsubscriptionexpirationmessaging_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.16 on 2024-10-31 13:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('subscriptions', '0073_remove_customeragreement_hyper_link_text_for_expired_modal_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalCustomSubscriptionExpirationMessaging', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('has_custom_license_expiration_messaging', models.BooleanField(default=False, help_text='Indicates if the customer has a unique license expiration experience, instead of the standard one.')), + ('modal_header_text', models.CharField(blank=True, help_text='The bold text that will appear as the header in the expiration modal.', max_length=512, null=True)), + ('expired_subscription_modal_messaging', models.TextField(blank=True, help_text='The content of a modal that will appear to learners upon subscription expiration. This text can be used for custom guidance per customer.', null=True)), + ('button_label_in_modal', models.CharField(blank=True, help_text='The text that will appear as on the button in the expiration modal', max_length=255, null=True)), + ('url_for_button_in_modal', models.CharField(blank=True, help_text='The URL that should underly the sole button in the expiration modal', max_length=512, null=True)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('customer_agreement', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='subscriptions.customeragreement')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical custom subscription expiration messaging', + 'verbose_name_plural': 'historical custom subscription expiration messagings', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='CustomSubscriptionExpirationMessaging', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('has_custom_license_expiration_messaging', models.BooleanField(default=False, help_text='Indicates if the customer has a unique license expiration experience, instead of the standard one.')), + ('modal_header_text', models.CharField(blank=True, help_text='The bold text that will appear as the header in the expiration modal.', max_length=512, null=True)), + ('expired_subscription_modal_messaging', models.TextField(blank=True, help_text='The content of a modal that will appear to learners upon subscription expiration. This text can be used for custom guidance per customer.', null=True)), + ('button_label_in_modal', models.CharField(blank=True, help_text='The text that will appear as on the button in the expiration modal', max_length=255, null=True)), + ('url_for_button_in_modal', models.CharField(blank=True, help_text='The URL that should underly the sole button in the expiration modal', max_length=512, null=True)), + ('customer_agreement', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='_custom_subscription_expiration_messaging', to='subscriptions.customeragreement')), + ], + ), + ] diff --git a/license_manager/apps/subscriptions/models.py b/license_manager/apps/subscriptions/models.py index b26071f7..5c0c4877 100644 --- a/license_manager/apps/subscriptions/models.py +++ b/license_manager/apps/subscriptions/models.py @@ -241,6 +241,16 @@ def auto_applicable_subscription(self): return plan + @property + def custom_subscription_expiration_messaging(self): + """ + Returns the custom subscription expiration messaging associated with this customer agreement. + """ + try: + return self._custom_subscription_expiration_messaging + except CustomSubscriptionExpirationMessaging.DoesNotExist: + return None + class Meta: verbose_name = _("Customer Agreement") verbose_name_plural = _("Customer Agreements") @@ -303,6 +313,66 @@ def __str__(self): ) +class CustomSubscriptionExpirationMessaging(models.Model): + """ + Custom subscription expiration messaging + + .. no_pii: This model has no PII + """ + + customer_agreement = models.OneToOneField( + CustomerAgreement, + on_delete=models.CASCADE, + related_name='_custom_subscription_expiration_messaging', + unique=True, + ) + + has_custom_license_expiration_messaging = models.BooleanField( + default=False, + help_text=_( + "Indicates if the customer has a unique license expiration experience, instead of the standard one." + ) + ) + + modal_header_text = models.CharField( + max_length=512, + blank=True, + null=True, + help_text=_( + "The bold text that will appear as the header in the expiration modal." + ) + ) + + expired_subscription_modal_messaging = models.TextField( + blank=True, + null=True, + help_text=_( + "The content of a modal that will appear to learners upon subscription expiration. This text can be used " + "for custom guidance per customer." + ) + ) + + button_label_in_modal = models.CharField( + max_length=255, + blank=True, + null=True, + help_text=_( + "The text that will appear as on the button in the expiration modal" + ) + ) + + url_for_button_in_modal = models.CharField( + max_length=512, + blank=True, + null=True, + help_text=_( + "The URL that should underly the sole button in the expiration modal" + ) + ) + + history = HistoricalRecords() + + class PlanType(models.Model): """ Stores top-level information related to available enterprise Subscription plan types. diff --git a/pylintrc b/pylintrc index 6edf14d3..290f0fb3 100644 --- a/pylintrc +++ b/pylintrc @@ -64,7 +64,7 @@ # SERIOUSLY. # # ------------------------------ -# Generated by edx-lint version: 5.4.0 +# Generated by edx-lint version: 5.4.1 # ------------------------------ [MASTER] ignore = ,migrations, settings, wsgi.py @@ -401,4 +401,4 @@ int-import-graph = [EXCEPTIONS] overgeneral-exceptions = builtins.Exception -# ea586deca5871e992466c748232382f9dfadff18 +# 3efa0bd414ae95120c9d8ac2ed13b2b5e1ed1e69