Skip to content

Commit 316fcd5

Browse files
committed
feat: make marketing email opt-in checkbox selectively ignorable
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 e5ebde8 commit 316fcd5

5 files changed

Lines changed: 364 additions & 15 deletions

File tree

.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 checkbox skip configuration field
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='skip_registration_optional_checkboxes',
17+
field=models.BooleanField(
18+
default=False,
19+
help_text=django.utils.translation.gettext_lazy(
20+
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
21+
"on the registration form for users registering via this provider. When these checkboxes "
22+
"are skipped, their values are inferred as False (opted out)."
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+
skip_registration_optional_checkboxes = models.BooleanField(
749+
default=False,
750+
help_text=_(
751+
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
752+
"on the registration form for users registering via this provider. When these checkboxes "
753+
"are skipped, their values are inferred as False (opted out)."
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: 123 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,56 @@ 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
419+
420+
def _get_saml_provider_config(self):
421+
"""
422+
Get the SAML provider config for the current request's running pipeline.
423+
424+
Returns:
425+
SAMLProviderConfig or None: The SAML provider config if found, None otherwise
426+
"""
427+
if not self.request or not third_party_auth.is_enabled():
428+
return None
429+
430+
running_pipeline = third_party_auth.pipeline.get(self.request)
431+
if not running_pipeline:
432+
return None
433+
434+
try:
435+
# idp_name can be in kwargs directly or in kwargs['details']
436+
saml_provider_name = running_pipeline.get('kwargs', {}).get('idp_name')
437+
if not saml_provider_name:
438+
saml_provider_name = (
439+
running_pipeline.get('kwargs', {})
440+
.get('details', {})
441+
.get('idp_name')
442+
)
443+
444+
if not saml_provider_name:
445+
return None
446+
447+
try:
448+
# Try to find the SAML provider config
449+
# First try with current_set(), then fall back to direct query
450+
try:
451+
return SAMLProviderConfig.objects.current_set().get(
452+
slug=saml_provider_name
453+
)
454+
except SAMLProviderConfig.DoesNotExist:
455+
# Fallback to direct query without current_set()
456+
return SAMLProviderConfig.objects.get(
457+
slug=saml_provider_name
458+
)
459+
except SAMLProviderConfig.DoesNotExist:
460+
log.debug(
461+
"SAML provider config not found for idp_name: %s",
462+
saml_provider_name
463+
)
464+
return None
465+
except Exception as exc: # pylint: disable=broad-except
466+
log.debug("Error getting SAML provider config: %s", str(exc))
467+
return None
413468

414469
def get_registration_form(self, request):
415470
"""Return a description of the registration form.
@@ -426,6 +481,7 @@ def get_registration_form(self, request):
426481
Returns:
427482
HttpResponse
428483
"""
484+
self.request = request
429485
form_desc = FormDescription("post", self._get_registration_submit_url(request))
430486
self._apply_third_party_auth_overrides(request, form_desc)
431487

@@ -703,13 +759,40 @@ def _add_marketing_emails_opt_in_field(self, form_desc, required=False):
703759
platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
704760
)
705761

762+
# Default: checkbox is checked
763+
default_value = True
764+
field_required = required
765+
766+
# Check if SAML provider wants to skip optional checkboxes
767+
saml_config = self._get_saml_provider_config()
768+
if saml_config and saml_config.skip_registration_optional_checkboxes:
769+
log.info(
770+
"SAML provider %s has skip_registration_optional_checkboxes=True, "
771+
"setting default to False and required to False",
772+
saml_config.slug
773+
)
774+
default_value = False # When skipped, user opts out by default
775+
field_required = False # Make field optional
776+
777+
# Set field override to ensure our SAML-specific values are used
778+
# This will override any values set by the TPA provider
779+
log.info(
780+
"Setting field override for marketing_emails_opt_in to "
781+
"defaultValue=False, required=False"
782+
)
783+
# pylint: disable=protected-access
784+
form_desc._field_overrides['marketing_emails_opt_in'] = {
785+
'defaultValue': False,
786+
'required': False
787+
}
788+
706789
form_desc.add_field(
707790
'marketing_emails_opt_in',
708791
label=opt_in_label,
709792
field_type="checkbox",
710793
exposed=True,
711-
default=True, # the checkbox will automatically be checked; meaning user has opted in
712-
required=required,
794+
default=default_value,
795+
required=field_required,
713796
)
714797

715798
def _add_field_with_configurable_select_options(self, field_name, field_label, form_desc, required=False):
@@ -1150,22 +1233,47 @@ def _apply_third_party_auth_overrides(self, request, form_desc):
11501233

11511234
for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS:
11521235
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-
):
1236+
# Special handling for marketing_emails_opt_in:
1237+
# If SAML provider config has skip_registration_optional_checkboxes=True,
1238+
# don't let the provider's get_register_form_data override the default
1239+
skip_override = False
1240+
if field_name == 'marketing_emails_opt_in':
1241+
saml_config = self._get_saml_provider_config()
1242+
if saml_config and saml_config.skip_registration_optional_checkboxes:
1243+
log.debug(
1244+
"Skipping provider override for marketing_emails_opt_in "
1245+
"due to SAML config for provider: %s",
1246+
saml_config.slug
1247+
)
1248+
skip_override = True
1249+
1250+
if not skip_override:
11621251
form_desc.override_field_properties(
1163-
field_name,
1164-
field_type="hidden",
1165-
label="",
1166-
instructions="",
1252+
field_name, default=field_overrides[field_name]
11671253
)
11681254

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

0 commit comments

Comments
 (0)