Skip to content

Commit e2044a6

Browse files
committed
feat: make marketing_emails_opt_in optional
We want to support a flow for SSO-enabled Enterprise customers who have agreed off-platform that none of their learners will opt-in to marketing emails or sharing research data. This change proposes to do so by adding an optional field that, when enabled, disables the presence of the two checkboxes on this registration form and sets their values to false. ENT-11401
1 parent ee35515 commit e2044a6

File tree

5 files changed

+369
-15
lines changed

5 files changed

+369
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ lms/envs/private.py
1313
cms/envs/private.py
1414
.venv/
1515
CLAUDE.md
16+
.claude/
1617
AGENTS.md
1718
# end-noclean
1819

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated migration for adding optional email checkbox configuration fields
2+
3+
from django.db import migrations, models
4+
import django.utils.translation
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('third_party_auth', '0013_default_site_id_wrapper_function'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='samlproviderconfig',
16+
name='marketing_emails_opt_in_optional',
17+
field=models.BooleanField(
18+
default=False,
19+
help_text=django.utils.translation.gettext_lazy(
20+
"If enabled, the marketing emails opt-in checkbox will be optional for users "
21+
"registering via this provider instead of required. When disabled, marketing email opt-in "
22+
"is determined by the global MARKETING_EMAILS_OPT_IN setting."
23+
),
24+
),
25+
),
26+
]

common/djangoapps/third_party_auth/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,14 @@ class SAMLProviderConfig(ProviderConfig):
745745
"immediately after authenticating with the third party instead of the login page."
746746
),
747747
)
748+
marketing_emails_opt_in_optional = models.BooleanField(
749+
default=False,
750+
help_text=_(
751+
"If enabled, the marketing emails opt-in checkbox will be optional for users "
752+
"registering via this provider instead of required. When disabled, marketing email opt-in "
753+
"is determined by the global MARKETING_EMAILS_OPT_IN setting."
754+
),
755+
)
748756
other_settings = models.TextField(
749757
verbose_name="Advanced settings", blank=True,
750758
help_text=(

openedx/core/djangoapps/user_authn/views/registration_form.py

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import copy
6+
import logging
67
import re
78
from importlib import import_module
89

@@ -18,6 +19,7 @@
1819
from eventtracking import tracker
1920

2021
from common.djangoapps import third_party_auth
22+
from common.djangoapps.third_party_auth.models import SAMLProviderConfig
2123
from common.djangoapps.edxmako.shortcuts import marketing_link
2224
from common.djangoapps.student.models import CourseEnrollmentAllowed, UserProfile, email_exists_or_retired
2325
from common.djangoapps.util.password_policy_validators import (
@@ -36,6 +38,9 @@
3638
from openedx.features.enterprise_support.api import enterprise_customer_for_request
3739

3840

41+
log = logging.getLogger(__name__)
42+
43+
3944
class TrueCheckbox(widgets.CheckboxInput):
4045
"""
4146
A checkbox widget that only accepts "true" (case-insensitive) as true.
@@ -410,6 +415,7 @@ def __init__(self):
410415
field_order.extend(sorted(difference))
411416

412417
self.field_order = field_order
418+
self.request = None # Will be set by get_registration_form
413419

414420
def get_registration_form(self, request):
415421
"""Return a description of the registration form.
@@ -426,6 +432,7 @@ def get_registration_form(self, request):
426432
Returns:
427433
HttpResponse
428434
"""
435+
self.request = request
429436
form_desc = FormDescription("post", self._get_registration_submit_url(request))
430437
self._apply_third_party_auth_overrides(request, form_desc)
431438

@@ -703,13 +710,65 @@ def _add_marketing_emails_opt_in_field(self, form_desc, required=False):
703710
platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
704711
)
705712

713+
# Check if marketing opt-in should be optional for this SAML provider
714+
# If the SAML provider config says the field is optional, set default to False
715+
# and clear any existing field overrides that may have been set by the TPA provider
716+
default_value = True # Default: checkbox is checked
717+
field_required = required
718+
719+
# pylint: disable=too-many-nested-blocks
720+
if self.request and third_party_auth.is_enabled():
721+
running_pipeline = third_party_auth.pipeline.get(self.request)
722+
if running_pipeline:
723+
try:
724+
# idp_name can be in kwargs directly or in kwargs['details']
725+
saml_provider_name = running_pipeline.get('kwargs', {}).get('idp_name')
726+
if not saml_provider_name:
727+
saml_provider_name = running_pipeline.get('kwargs', {}).get('details', {}).get('idp_name')
728+
729+
if saml_provider_name:
730+
try:
731+
# Query the SAML provider config
732+
saml_config = SAMLProviderConfig.objects.get(
733+
slug=saml_provider_name
734+
)
735+
736+
if saml_config.marketing_emails_opt_in_optional:
737+
log.info(
738+
"SAML provider %s has marketing_emails_opt_in_optional=True, "
739+
"setting default to False and required to False",
740+
saml_provider_name
741+
)
742+
default_value = False # When optional, user opts out by default
743+
field_required = False # Make field optional
744+
745+
# Set field override to ensure our SAML-specific values are used
746+
# This will override any values set by the TPA provider, or create
747+
# a new override if one doesn't exist
748+
log.info(
749+
"Setting field override for marketing_emails_opt_in to "
750+
"defaultValue=False, required=False"
751+
)
752+
# pylint: disable=protected-access
753+
form_desc._field_overrides['marketing_emails_opt_in'] = {
754+
'defaultValue': False,
755+
'required': False
756+
}
757+
except SAMLProviderConfig.DoesNotExist:
758+
log.exception(
759+
"SAML provider config not found for idp_name: %s",
760+
saml_provider_name
761+
)
762+
except Exception as exc: # pylint: disable=broad-except
763+
log.exception("Error checking SAML provider config in field handler: %s", str(exc))
764+
706765
form_desc.add_field(
707766
'marketing_emails_opt_in',
708767
label=opt_in_label,
709768
field_type="checkbox",
710769
exposed=True,
711-
default=True, # the checkbox will automatically be checked; meaning user has opted in
712-
required=required,
770+
default=default_value,
771+
required=field_required,
713772
)
714773

715774
def _add_field_with_configurable_select_options(self, field_name, field_label, form_desc, required=False):
@@ -1150,22 +1209,76 @@ def _apply_third_party_auth_overrides(self, request, form_desc):
11501209

11511210
for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS:
11521211
if field_name in field_overrides:
1153-
form_desc.override_field_properties(
1154-
field_name, default=field_overrides[field_name]
1155-
)
1156-
1157-
if (
1158-
field_name not in ['terms_of_service', 'honor_code'] and
1159-
field_overrides[field_name] and
1160-
hide_registration_fields_except_tos
1161-
):
1212+
# Special handling for marketing_emails_opt_in:
1213+
# If SAML provider config has set marketing_emails_opt_in_optional=True,
1214+
# don't let the provider's get_register_form_data override the default
1215+
skip_override = False
1216+
if field_name == 'marketing_emails_opt_in':
1217+
try:
1218+
# idp_name can be in kwargs directly or in kwargs['details']
1219+
saml_provider_name = running_pipeline.get('kwargs', {}).get('idp_name')
1220+
if not saml_provider_name:
1221+
saml_provider_name = (
1222+
running_pipeline.get('kwargs', {})
1223+
.get('details', {})
1224+
.get('idp_name')
1225+
)
1226+
if saml_provider_name:
1227+
try:
1228+
# Try to find the SAML provider config
1229+
# First try with current_set(), then fall back to direct query
1230+
try:
1231+
saml_config = SAMLProviderConfig.objects.current_set().get(
1232+
slug=saml_provider_name
1233+
)
1234+
except SAMLProviderConfig.DoesNotExist:
1235+
# Fallback to direct query without current_set()
1236+
saml_config = SAMLProviderConfig.objects.get(
1237+
slug=saml_provider_name
1238+
)
1239+
1240+
if saml_config.marketing_emails_opt_in_optional:
1241+
log.debug(
1242+
"Skipping provider override for marketing_emails_opt_in "
1243+
"due to SAML config for provider: %s",
1244+
saml_provider_name
1245+
)
1246+
skip_override = True
1247+
except SAMLProviderConfig.DoesNotExist:
1248+
log.exception(
1249+
"SAML provider config not found for idp_name: %s",
1250+
saml_provider_name
1251+
)
1252+
except Exception as exc: # pylint: disable=broad-except
1253+
log.exception("Error checking SAML provider config: %s", str(exc))
1254+
1255+
if not skip_override:
11621256
form_desc.override_field_properties(
1163-
field_name,
1164-
field_type="hidden",
1165-
label="",
1166-
instructions="",
1257+
field_name, default=field_overrides[field_name]
11671258
)
11681259

1260+
if (
1261+
field_name not in ['terms_of_service', 'honor_code'] and
1262+
field_overrides[field_name] and
1263+
hide_registration_fields_except_tos
1264+
):
1265+
# When hiding a field, set default to False for checkbox fields
1266+
# like marketing_emails_opt_in to avoid auto-opting users in
1267+
field_default = field_overrides[field_name]
1268+
if field_name == 'marketing_emails_opt_in':
1269+
field_default = False
1270+
log.info(
1271+
"Hiding marketing_emails_opt_in field and setting default to False"
1272+
)
1273+
1274+
form_desc.override_field_properties(
1275+
field_name,
1276+
field_type="hidden",
1277+
default=field_default,
1278+
label="",
1279+
instructions="",
1280+
)
1281+
11691282
# Hide the confirm_email field
11701283
form_desc.override_field_properties(
11711284
"confirm_email",

0 commit comments

Comments
 (0)