From 11ae7f3de35db50108f92dc0b20d9eeffc552fa6 Mon Sep 17 00:00:00 2001 From: jajjibhai008 Date: Mon, 14 Oct 2024 16:54:59 +0500 Subject: [PATCH] feat: update some fields in customer agreemnet model --- license_manager/apps/api/serializers.py | 5 +- license_manager/apps/subscriptions/admin.py | 7 +- ...greement_button_label_in_modal_and_more.py | 53 ++++++++ license_manager/apps/subscriptions/models.py | 116 +++++++++++------- .../apps/subscriptions/sanitize.py | 30 +++++ requirements/base.in | 1 + requirements/base.txt | 13 +- requirements/dev.txt | 17 ++- requirements/doc.txt | 17 ++- requirements/pip.txt | 2 +- requirements/production.txt | 15 ++- requirements/quality.txt | 15 ++- requirements/test.txt | 17 ++- requirements/validation.txt | 20 ++- 14 files changed, 250 insertions(+), 78 deletions(-) create mode 100644 license_manager/apps/subscriptions/migrations/0072_customeragreement_button_label_in_modal_and_more.py create mode 100644 license_manager/apps/subscriptions/sanitize.py diff --git a/license_manager/apps/api/serializers.py b/license_manager/apps/api/serializers.py index 38a56e78..5123f0bf 100644 --- a/license_manager/apps/api/serializers.py +++ b/license_manager/apps/api/serializers.py @@ -354,11 +354,14 @@ class Meta: 'net_days_until_expiration', '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', 'hyper_link_text_for_expired_modal', 'url_for_expired_modal', - 'enable_auto_applied_subscriptions_with_universal_link' ] def get_subscription_for_auto_applied_licenses(self, obj): diff --git a/license_manager/apps/subscriptions/admin.py b/license_manager/apps/subscriptions/admin.py index 0697816a..522a28a1 100644 --- a/license_manager/apps/subscriptions/admin.py +++ b/license_manager/apps/subscriptions/admin.py @@ -417,11 +417,14 @@ class CustomerAgreementAdmin(admin.ModelAdmin): 'disable_expiration_notifications', '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', 'hyper_link_text_for_expired_modal', - 'url_for_expired_modal', - 'enable_auto_applied_subscriptions_with_universal_link' + 'url_for_expired_modal' ) custom_fields = ('subscription_for_auto_applied_licenses',) diff --git a/license_manager/apps/subscriptions/migrations/0072_customeragreement_button_label_in_modal_and_more.py b/license_manager/apps/subscriptions/migrations/0072_customeragreement_button_label_in_modal_and_more.py new file mode 100644 index 00000000..623c0f4f --- /dev/null +++ b/license_manager/apps/subscriptions/migrations/0072_customeragreement_button_label_in_modal_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.16 on 2024-10-18 09:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('subscriptions', '0071_customeragreement_enable_auto_applied_subscriptions_with_universal_link_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='customeragreement', + name='button_label_in_modal', + field=models.CharField(blank=True, help_text='The text that will appear as on the button in the expiration modal', max_length=255, null=True), + ), + migrations.AddField( + model_name='customeragreement', + name='modal_header_text', + field=models.CharField(blank=True, help_text='The bold text that will appear as the header in the expiration modal.', max_length=512, null=True), + ), + migrations.AddField( + model_name='customeragreement', + name='url_for_button_in_modal', + field=models.CharField(blank=True, help_text='The URL that should underly the sole button in the expiration modal', max_length=512, null=True), + ), + migrations.AddField( + model_name='historicalcustomeragreement', + name='button_label_in_modal', + field=models.CharField(blank=True, help_text='The text that will appear as on the button in the expiration modal', max_length=255, null=True), + ), + migrations.AddField( + model_name='historicalcustomeragreement', + name='modal_header_text', + field=models.CharField(blank=True, help_text='The bold text that will appear as the header in the expiration modal.', max_length=512, null=True), + ), + migrations.AddField( + model_name='historicalcustomeragreement', + name='url_for_button_in_modal', + field=models.CharField(blank=True, help_text='The URL that should underly the sole button in the expiration modal', max_length=512, null=True), + ), + migrations.AlterField( + model_name='customeragreement', + name='expired_subscription_modal_messaging', + field=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), + ), + migrations.AlterField( + model_name='historicalcustomeragreement', + name='expired_subscription_modal_messaging', + field=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), + ), + ] diff --git a/license_manager/apps/subscriptions/models.py b/license_manager/apps/subscriptions/models.py index edffda87..e786e8af 100644 --- a/license_manager/apps/subscriptions/models.py +++ b/license_manager/apps/subscriptions/models.py @@ -48,6 +48,7 @@ track_event, track_license_changes, ) +from license_manager.apps.subscriptions.sanitize import sanitize_html from license_manager.apps.subscriptions.utils import ( days_until, get_license_activation_link, @@ -150,8 +151,16 @@ class CustomerAgreement(TimeStampedModel): ) ) - expired_subscription_modal_messaging = models.CharField( + 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=_( @@ -178,6 +187,24 @@ class CustomerAgreement(TimeStampedModel): ) ) + 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" + ) + ) + enable_auto_applied_subscriptions_with_universal_link = models.BooleanField( default=False, help_text=_( @@ -237,54 +264,59 @@ class Meta: verbose_name_plural = _("Customer Agreements") def clean(self): - # Check if custom messaging is enabled and messaging field is blank + """ + Custom clean method to validate fields based on the 'Has Custom License Expiration Messaging' flag. + """ + errors = {} + + # Sanitize the expired_subscription_modal_messaging field + if self.expired_subscription_modal_messaging: + self.expired_subscription_modal_messaging = sanitize_html(self.expired_subscription_modal_messaging) + + error_message = "This field cannot be blank if 'Has Custom License Expiration Messaging' is checked." + # Validate fields when custom messaging is enabled if self.has_custom_license_expiration_messaging: - if not self.expired_subscription_modal_messaging: - raise ValidationError({ - "expired_subscription_modal_messaging": ( - "This field cannot be blank if 'Has Custom License Expiration Messaging' is checked." - ) - }) - - # Validate that URL field is not blank if hyperlink text is provided - if self.hyper_link_text_for_expired_modal and not self.url_for_expired_modal: - raise ValidationError({ - "url_for_expired_modal": ( - "This field cannot be blank if 'Hyper Link Text for Expired Modal' has values." - ) - }) + required_fields = { + "modal_header_text": error_message, + "expired_subscription_modal_messaging": error_message, + "button_label_in_modal": error_message, + "url_for_button_in_modal": error_message, + "hyper_link_text_for_expired_modal": error_message, + "url_for_expired_modal": error_message + } - # Validate that hyperlink text is not blank if URL is provided - if self.url_for_expired_modal and not self.hyper_link_text_for_expired_modal: - raise ValidationError({ - "hyper_link_text_for_expired_modal": ( - "This field cannot be blank if 'URL for Expired Modal' has values." - ) - }) + # Check if any required fields are missing + for field, error_message in required_fields.items(): + if not getattr(self, field): + errors[field] = error_message # Ensure all fields are blank if custom messaging is disabled if not self.has_custom_license_expiration_messaging: - if any([ - self.expired_subscription_modal_messaging, - self.hyper_link_text_for_expired_modal, - self.url_for_expired_modal - ]): + fields_to_check = [ + "modal_header_text", + "expired_subscription_modal_messaging", + "button_label_in_modal", + "url_for_button_in_modal", + "hyper_link_text_for_expired_modal", + "url_for_expired_modal", + ] + if any(getattr(self, field) for field in fields_to_check): error_msg = "This field must be blank if 'Has Custom License Expiration Messaging' is unchecked." - raise ValidationError({ - "expired_subscription_modal_messaging": error_msg, - "hyper_link_text_for_expired_modal": error_msg, - "url_for_expired_modal": error_msg, - }) - - def __str__(self): - """ - Return human-readable string representation. - """ - return ( - "".format( - self.enterprise_customer_slug or self.enterprise_customer_name - ) + errors = {field: error_msg for field in fields_to_check} + + # Raise ValidationError if there are any errors + if errors: + raise ValidationError(errors) + + def __str__(self): + """ + Return human-readable string representation. + """ + return ( + "".format( + self.enterprise_customer_slug or self.enterprise_customer_name ) + ) class PlanType(models.Model): diff --git a/license_manager/apps/subscriptions/sanitize.py b/license_manager/apps/subscriptions/sanitize.py new file mode 100644 index 00000000..c2da423b --- /dev/null +++ b/license_manager/apps/subscriptions/sanitize.py @@ -0,0 +1,30 @@ +import bleach + + +def sanitize_html(html_content): + """ + Sanitize HTML content to allow only safe tags and attributes, + while disallowing JavaScript and unsafe protocols. + """ + # Define allowed tags and attributes + allowed_tags = bleach.ALLOWED_TAGS # Allow all standard HTML tags + allowed_attrs = {"*": ["className", "class", "style", "id"]} + + # Clean the HTML content + sanitized_content = bleach.clean( + html_content, + tags=allowed_tags, + attributes=allowed_attrs, + strip=True, # Strip disallowed tags completely + protocols=["http", "https"], # Only allow http and https URLs + ) + + # Use bleach.linkify to ensure no javascript: links in tags + sanitized_content = bleach.linkify( + sanitized_content, + callbacks=[ + bleach.callbacks.nofollow + ], # Apply 'nofollow' to external links for safety + ) + + return sanitized_content diff --git a/requirements/base.in b/requirements/base.in index e7bdd14f..6de2a2d5 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -40,3 +40,4 @@ rules simplejson zipp django-log-request-id +bleach diff --git a/requirements/base.txt b/requirements/base.txt index 17585d69..7800d2c1 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -22,9 +22,11 @@ backoff==1.10.0 # analytics-python billiard==4.2.1 # via celery -boto3==1.35.39 +bleach==6.1.0 + # via -r requirements/base.in +boto3==1.35.42 # via django-ses -botocore==1.35.39 +botocore==1.35.42 # via # boto3 # s3transfer @@ -153,7 +155,7 @@ edx-braze-client==0.2.5 # via -r requirements/base.in edx-celeryutils==1.3.0 # via -r requirements/base.in -edx-django-utils==6.0.0 +edx-django-utils==6.1.0 # via # -r requirements/base.in # edx-drf-extensions @@ -195,7 +197,7 @@ monotonic==1.6 # via analytics-python mysqlclient==2.2.4 # via -r requirements/base.in -newrelic==10.1.0 +newrelic==10.2.0 # via edx-django-utils oauthlib==3.2.2 # via @@ -267,6 +269,7 @@ simplejson==3.19.3 six==1.16.0 # via # analytics-python + # bleach # edx-auth-backends # edx-rbac # python-dateutil @@ -304,6 +307,8 @@ vine==5.1.0 # kombu wcwidth==0.2.13 # via prompt-toolkit +webencodings==0.5.1 + # via bleach zipp==3.20.2 # via -r requirements/base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 6aea12d8..84f5cc3d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -33,11 +33,13 @@ billiard==4.2.1 # via # -r requirements/validation.txt # celery -boto3==1.35.39 +bleach==6.1.0 + # via -r requirements/validation.txt +boto3==1.35.42 # via # -r requirements/validation.txt # django-ses -botocore==1.35.39 +botocore==1.35.42 # via # -r requirements/validation.txt # boto3 @@ -225,7 +227,7 @@ edx-braze-client==0.2.5 # via -r requirements/validation.txt edx-celeryutils==1.3.0 # via -r requirements/validation.txt -edx-django-utils==6.0.0 +edx-django-utils==6.1.0 # via # -r requirements/validation.txt # edx-drf-extensions @@ -251,7 +253,7 @@ edx-toggles==5.2.0 # via -r requirements/validation.txt factory-boy==3.3.1 # via -r requirements/validation.txt -faker==30.3.0 +faker==30.6.0 # via # -r requirements/validation.txt # factory-boy @@ -333,7 +335,7 @@ more-itertools==10.5.0 # via inflect mysqlclient==2.2.4 # via -r requirements/validation.txt -newrelic==10.1.0 +newrelic==10.2.0 # via # -r requirements/validation.txt # edx-django-utils @@ -513,6 +515,7 @@ six==1.16.0 # via # -r requirements/validation.txt # analytics-python + # bleach # edx-auth-backends # edx-lint # edx-rbac @@ -582,6 +585,10 @@ wcwidth==0.2.13 # via # -r requirements/validation.txt # prompt-toolkit +webencodings==0.5.1 + # via + # -r requirements/validation.txt + # bleach wheel==0.44.0 # via # -r requirements/pip-tools.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 11f2e46e..54859157 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -43,11 +43,13 @@ billiard==4.2.1 # via # -r requirements/test.txt # celery -boto3==1.35.39 +bleach==6.1.0 + # via -r requirements/test.txt +boto3==1.35.42 # via # -r requirements/test.txt # django-ses -botocore==1.35.39 +botocore==1.35.42 # via # -r requirements/test.txt # boto3 @@ -226,7 +228,7 @@ edx-braze-client==0.2.5 # via -r requirements/test.txt edx-celeryutils==1.3.0 # via -r requirements/test.txt -edx-django-utils==6.0.0 +edx-django-utils==6.1.0 # via # -r requirements/test.txt # edx-drf-extensions @@ -250,7 +252,7 @@ edx-toggles==5.2.0 # via -r requirements/test.txt factory-boy==3.3.1 # via -r requirements/test.txt -faker==30.3.0 +faker==30.6.0 # via # -r requirements/test.txt # factory-boy @@ -314,7 +316,7 @@ monotonic==1.6 # analytics-python mysqlclient==2.2.4 # via -r requirements/test.txt -newrelic==10.1.0 +newrelic==10.2.0 # via # -r requirements/test.txt # edx-django-utils @@ -481,6 +483,7 @@ six==1.16.0 # via # -r requirements/test.txt # analytics-python + # bleach # edx-auth-backends # edx-lint # edx-rbac @@ -566,6 +569,10 @@ wcwidth==0.2.13 # via # -r requirements/test.txt # prompt-toolkit +webencodings==0.5.1 + # via + # -r requirements/test.txt + # bleach zipp==3.20.2 # via -r requirements/test.txt diff --git a/requirements/pip.txt b/requirements/pip.txt index 488d41f8..35655630 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -10,5 +10,5 @@ wheel==0.44.0 # The following packages are considered to be unsafe in a requirements file: pip==24.2 # via -r requirements/pip.in -setuptools==75.1.0 +setuptools==75.2.0 # via -r requirements/pip.in diff --git a/requirements/production.txt b/requirements/production.txt index e06bc8a5..08ca91b5 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -28,11 +28,13 @@ billiard==4.2.1 # via # -r requirements/base.txt # celery -boto3==1.35.39 +bleach==6.1.0 + # via -r requirements/base.txt +boto3==1.35.42 # via # -r requirements/base.txt # django-ses -botocore==1.35.39 +botocore==1.35.42 # via # -r requirements/base.txt # boto3 @@ -183,7 +185,7 @@ edx-braze-client==0.2.5 # via -r requirements/base.txt edx-celeryutils==1.3.0 # via -r requirements/base.txt -edx-django-utils==6.0.0 +edx-django-utils==6.1.0 # via # -r requirements/base.txt # edx-drf-extensions @@ -252,7 +254,7 @@ monotonic==1.6 # analytics-python mysqlclient==2.2.4 # via -r requirements/base.txt -newrelic==10.1.0 +newrelic==10.2.0 # via # -r requirements/base.txt # edx-django-utils @@ -365,6 +367,7 @@ six==1.16.0 # via # -r requirements/base.txt # analytics-python + # bleach # edx-auth-backends # edx-rbac # python-dateutil @@ -419,6 +422,10 @@ wcwidth==0.2.13 # via # -r requirements/base.txt # prompt-toolkit +webencodings==0.5.1 + # via + # -r requirements/base.txt + # bleach zipp==3.20.2 # via -r requirements/base.txt zope-event==5.0 diff --git a/requirements/quality.txt b/requirements/quality.txt index 75979165..bbad69ae 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -32,11 +32,13 @@ billiard==4.2.1 # via # -r requirements/base.txt # celery -boto3==1.35.39 +bleach==6.1.0 + # via -r requirements/base.txt +boto3==1.35.42 # via # -r requirements/base.txt # django-ses -botocore==1.35.39 +botocore==1.35.42 # via # -r requirements/base.txt # boto3 @@ -194,7 +196,7 @@ edx-braze-client==0.2.5 # via -r requirements/base.txt edx-celeryutils==1.3.0 # via -r requirements/base.txt -edx-django-utils==6.0.0 +edx-django-utils==6.1.0 # via # -r requirements/base.txt # edx-drf-extensions @@ -265,7 +267,7 @@ monotonic==1.6 # analytics-python mysqlclient==2.2.4 # via -r requirements/base.txt -newrelic==10.1.0 +newrelic==10.2.0 # via # -r requirements/base.txt # edx-django-utils @@ -391,6 +393,7 @@ six==1.16.0 # via # -r requirements/base.txt # analytics-python + # bleach # edx-auth-backends # edx-lint # edx-rbac @@ -450,6 +453,10 @@ wcwidth==0.2.13 # via # -r requirements/base.txt # prompt-toolkit +webencodings==0.5.1 + # via + # -r requirements/base.txt + # bleach zipp==3.20.2 # via -r requirements/base.txt diff --git a/requirements/test.txt b/requirements/test.txt index 00efff69..ed139657 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -32,11 +32,13 @@ billiard==4.2.1 # via # -r requirements/base.txt # celery -boto3==1.35.39 +bleach==6.1.0 + # via -r requirements/base.txt +boto3==1.35.42 # via # -r requirements/base.txt # django-ses -botocore==1.35.39 +botocore==1.35.42 # via # -r requirements/base.txt # boto3 @@ -203,7 +205,7 @@ edx-braze-client==0.2.5 # via -r requirements/base.txt edx-celeryutils==1.3.0 # via -r requirements/base.txt -edx-django-utils==6.0.0 +edx-django-utils==6.1.0 # via # -r requirements/base.txt # edx-drf-extensions @@ -227,7 +229,7 @@ edx-toggles==5.2.0 # via -r requirements/base.txt factory-boy==3.3.1 # via -r requirements/test.in -faker==30.3.0 +faker==30.6.0 # via factory-boy freezegun==1.5.1 # via -r requirements/test.in @@ -280,7 +282,7 @@ monotonic==1.6 # analytics-python mysqlclient==2.2.4 # via -r requirements/base.txt -newrelic==10.1.0 +newrelic==10.2.0 # via # -r requirements/base.txt # edx-django-utils @@ -416,6 +418,7 @@ six==1.16.0 # via # -r requirements/base.txt # analytics-python + # bleach # edx-auth-backends # edx-lint # edx-rbac @@ -474,6 +477,10 @@ wcwidth==0.2.13 # via # -r requirements/base.txt # prompt-toolkit +webencodings==0.5.1 + # via + # -r requirements/base.txt + # bleach zipp==3.20.2 # via -r requirements/base.txt diff --git a/requirements/validation.txt b/requirements/validation.txt index 2ec1a3e8..d49c6103 100644 --- a/requirements/validation.txt +++ b/requirements/validation.txt @@ -41,12 +41,16 @@ billiard==4.2.1 # -r requirements/quality.txt # -r requirements/test.txt # celery -boto3==1.35.39 +bleach==6.1.0 + # via + # -r requirements/quality.txt + # -r requirements/test.txt +boto3==1.35.42 # via # -r requirements/quality.txt # -r requirements/test.txt # django-ses -botocore==1.35.39 +botocore==1.35.42 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -270,7 +274,7 @@ edx-celeryutils==1.3.0 # via # -r requirements/quality.txt # -r requirements/test.txt -edx-django-utils==6.0.0 +edx-django-utils==6.1.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -307,7 +311,7 @@ edx-toggles==5.2.0 # -r requirements/test.txt factory-boy==3.3.1 # via -r requirements/test.txt -faker==30.3.0 +faker==30.6.0 # via # -r requirements/test.txt # factory-boy @@ -388,7 +392,7 @@ mysqlclient==2.2.4 # via # -r requirements/quality.txt # -r requirements/test.txt -newrelic==10.1.0 +newrelic==10.2.0 # via # -r requirements/quality.txt # -r requirements/test.txt @@ -584,6 +588,7 @@ six==1.16.0 # -r requirements/quality.txt # -r requirements/test.txt # analytics-python + # bleach # edx-auth-backends # edx-lint # edx-rbac @@ -661,6 +666,11 @@ wcwidth==0.2.13 # -r requirements/quality.txt # -r requirements/test.txt # prompt-toolkit +webencodings==0.5.1 + # via + # -r requirements/quality.txt + # -r requirements/test.txt + # bleach zipp==3.20.2 # via # -r requirements/quality.txt