Skip to content

Commit

Permalink
feat: update some fields in customer agreemnet model
Browse files Browse the repository at this point in the history
  • Loading branch information
jajjibhai008 committed Oct 16, 2024
1 parent 9b54b8d commit b2ea305
Show file tree
Hide file tree
Showing 14 changed files with 294 additions and 132 deletions.
5 changes: 3 additions & 2 deletions license_manager/apps/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,10 @@ class Meta:
'subscription_for_auto_applied_licenses',
'available_subscription_catalogs',
'has_custom_license_expiration_messaging',
'modal_header_text',
'expired_subscription_modal_messaging',
'hyper_link_text_for_expired_modal',
'url_for_expired_modal',
'button_label_in_modal',
'url_for_button_in_modal',
'enable_auto_applied_subscriptions_with_universal_link'
]

Expand Down
5 changes: 3 additions & 2 deletions license_manager/apps/subscriptions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,10 @@ class CustomerAgreementAdmin(admin.ModelAdmin):
'license_duration_before_purge',
'disable_onboarding_notifications',
'has_custom_license_expiration_messaging',
'modal_header_text',
'expired_subscription_modal_messaging',
'hyper_link_text_for_expired_modal',
'url_for_expired_modal',
'button_label_in_modal',
'url_for_button_in_modal',
'enable_auto_applied_subscriptions_with_universal_link'
)
custom_fields = ('subscription_for_auto_applied_licenses',)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Generated by Django 4.2.16 on 2024-10-14 10:58

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.RemoveField(
model_name='customeragreement',
name='hyper_link_text_for_expired_modal',
),
migrations.RemoveField(
model_name='customeragreement',
name='url_for_expired_modal',
),
migrations.RemoveField(
model_name='historicalcustomeragreement',
name='hyper_link_text_for_expired_modal',
),
migrations.RemoveField(
model_name='historicalcustomeragreement',
name='url_for_expired_modal',
),
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),
),
]
102 changes: 56 additions & 46 deletions license_manager/apps/subscriptions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
provision_licenses,
)

from license_manager.apps.subscriptions.sanitize import sanitize_html
from .exceptions import (
LicenseActivationMissingError,
LicenseToActivateIsRevokedError,
Expand Down Expand Up @@ -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=_(
Expand All @@ -160,21 +169,21 @@ class CustomerAgreement(TimeStampedModel):
)
)

hyper_link_text_for_expired_modal = models.CharField(
button_label_in_modal = models.CharField(
max_length=255,
blank=True,
null=True,
help_text=_(
"The display text for the link that will be embedded at the end of the custom expiration modal."
"The text that will appear as on the button in the expiration modal"
)
)

url_for_expired_modal = models.CharField(
url_for_button_in_modal = models.CharField(
max_length=512,
blank=True,
null=True,
help_text=_(
"The underlying url that will be embedded as a hyperlink at the end of the custom expiration modal."
"The URL that should underly the sole button in the expiration modal"
)
)

Expand Down Expand Up @@ -237,54 +246,55 @@ 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,
}

# 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"
]
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 (
"<CustomerAgreement: '{}'>".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 (
"<CustomerAgreement: '{}'>".format(
self.enterprise_customer_slug or self.enterprise_customer_name
)
)


class PlanType(models.Model):
Expand Down
30 changes: 30 additions & 0 deletions license_manager/apps/subscriptions/sanitize.py
Original file line number Diff line number Diff line change
@@ -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 <a> tags
sanitized_content = bleach.linkify(
sanitized_content,
callbacks=[
bleach.callbacks.nofollow
], # Apply 'nofollow' to external links for safety
)

return sanitized_content
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ rules
simplejson
zipp
django-log-request-id
bleach
23 changes: 14 additions & 9 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ backoff==1.10.0
# analytics-python
billiard==4.2.1
# via celery
boto3==1.35.34
bleach==6.1.0
# via -r requirements/base.in
boto3==1.35.41
# via django-ses
botocore==1.35.34
botocore==1.35.41
# via
# boto3
# s3transfer
Expand All @@ -39,7 +41,7 @@ cffi==1.17.1
# via
# cryptography
# pynacl
charset-normalizer==3.3.2
charset-normalizer==3.4.0
# via requests
click==8.1.7
# via
Expand Down Expand Up @@ -97,7 +99,7 @@ django-autocomplete-light==3.11.0
# via -r requirements/base.in
django-celery-results==2.5.1
# via -r requirements/base.in
django-cors-headers==4.4.0
django-cors-headers==4.5.0
# via -r requirements/base.in
django-crum==0.7.9
# via
Expand Down Expand Up @@ -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==5.16.0
edx-django-utils==6.1.0
# via
# -r requirements/base.in
# edx-drf-extensions
Expand Down Expand Up @@ -185,17 +187,17 @@ jsonfield==3.1.0
# via edx-celeryutils
jsonschema==4.23.0
# via drf-spectacular
jsonschema-specifications==2023.12.1
jsonschema-specifications==2024.10.1
# via jsonschema
kombu==5.4.2
# via celery
markupsafe==2.1.5
markupsafe==3.0.1
# via jinja2
monotonic==1.6
# via analytics-python
mysqlclient==2.2.4
# via -r requirements/base.in
newrelic==10.0.0
newrelic==10.2.0
# via edx-django-utils
oauthlib==3.2.2
# via
Expand Down Expand Up @@ -258,7 +260,7 @@ rpds-py==0.20.0
# referencing
rules==3.5
# via -r requirements/base.in
s3transfer==0.10.2
s3transfer==0.10.3
# via boto3
semantic-version==2.10.0
# via edx-drf-extensions
Expand All @@ -267,6 +269,7 @@ simplejson==3.19.3
six==1.16.0
# via
# analytics-python
# bleach
# edx-auth-backends
# edx-rbac
# python-dateutil
Expand Down Expand Up @@ -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

Expand Down
Loading

0 comments on commit b2ea305

Please sign in to comment.