feat: make marketing email and research opt-in checkboxs 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
This commit is contained in:
Alexander Dusenbery
2026-02-23 11:11:14 -05:00
committed by Alex Dusenbery
parent 5e36a38569
commit 2fcce121e9
10 changed files with 655 additions and 16 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ lms/envs/private.py
cms/envs/private.py
.venv/
CLAUDE.md
.claude/
AGENTS.md
# end-noclean

View File

@@ -0,0 +1,156 @@
Testing SAML Authentication Locally with MockSAML
==================================================
This guide walks through setting up and testing SAML authentication in a local Open edX devstack environment using MockSAML.com as a test Identity Provider (IdP).
Overview
--------
SAML (Security Assertion Markup Language) authentication in Open edX requires three configuration objects to work together:
1. **SAMLConfiguration**: Configures the Service Provider (SP) metadata - entity ID, keys, and organization info
2. **SAMLProviderConfig**: Configures a specific Identity Provider (IdP) connection with metadata URL and attribute mappings
3. **SAMLProviderData**: Stores the IdP's metadata (SSO URL, public key) fetched from the IdP's metadata endpoint
**Critical Requirement**: The SAMLConfiguration object MUST have the slug "default" because this value is hardcoded in the authentication execution path at ``common/djangoapps/third_party_auth/models.py:906``.
Prerequisites
-------------
* Local Open edX devstack running
* Access to Django admin at http://localhost:18000/admin/
* MockSAML.com account (free service for SAML testing)
Step 1: Configure SAMLConfiguration
------------------------------------
The SAMLConfiguration defines your Open edX instance as a SAML Service Provider (SP).
1. Navigate to Django Admin → Third Party Auth → SAML Configurations
2. Click "Add SAML Configuration"
3. Configure with these **required** values:
============ ===================================================
Field Value
============ ===================================================
Site localhost:18000
**Slug** **default** (MUST be "default" - hardcoded in code)
Entity ID https://saml.example.com/entityid
Enabled ✓ (checked)
============ ===================================================
4. For local testing with MockSAML, you can leave the keys blank.
5. Optionally configure Organization Info (use default or customize):
.. code-block:: json
{
"en-US": {
"url": "http://localhost:18000",
"displayname": "Local Open edX",
"name": "localhost"
}
}
6. Click "Save"
Step 2: Configure SAMLProviderConfig
-------------------------------------
The SAMLProviderConfig connects to a specific SAML Identity Provider (MockSAML in this case).
1. Navigate to Django Admin → Third Party Auth → Provider Configuration (SAML IdPs)
2. Click "Add Provider Configuration (SAML IdP)"
3. Configure with these values:
========================= ===================================================
Field Value
========================= ===================================================
Name Test Localhost (or any descriptive name)
Slug default (to match test URLs)
Backend Name tpa-saml
Entity ID https://saml.example.com/entityid
Metadata Source https://mocksaml.com/api/saml/metadata
Site localhost:18000
SAML Configuration Select the SAMLConfiguration created in Step 1
Enabled ✓ (checked)
Visible ☐ (unchecked for testing)
Skip hinted login dialog ✓ (checked - recommended)
Skip registration form ✓ (checked - recommended)
Skip email verification ✓ (checked - recommended)
Send to registration first ✓ (checked - recommended)
========================= ===================================================
4. Leave all attribute mappings (User ID, Email, Full Name, etc.) blank to use defaults
5. Click "Save"
**Important**: The Entity ID in SAMLProviderConfig MUST match the Entity ID in SAMLConfiguration.
Step 3: Set IdP Data
--------------------
The SAMLProviderData stores metadata from the Identity Provider (MockSAML), create a record with
* **Entity ID**: https://saml.example.com/entityid
* **SSO URL**: https://mocksaml.com/api/saml/sso
* **Public Key**: The IdP's signing certificate
* **Expires At**: Set to 1 year from fetch time
Step 4: Test SAML Authentication
---------------------------------
1. Navigate to: http://localhost:18000/auth/idp_redirect/saml-default
2. You should be redirected to MockSAML.com
3. Complete the authentication on MockSAML - just click "Sign In" with whatever is in the form.
4. You should be redirected back to Open edX
5. If this is a new user, you'll see the registration form
6. After registration, you should be logged in
Expected Behavior
^^^^^^^^^^^^^^^^^
1. Initial redirect to MockSAML (https://mocksaml.com/api/saml/sso)
2. MockSAML displays the login page
3. After authentication, MockSAML POSTs the SAML assertion back to Open edX
4. Open edX validates the assertion and creates/logs in the user
5. User is redirected to the dashboard or registration form (if new user)
Reference Configuration
-----------------------
Here's a summary of a working test configuration:
**SAMLConfiguration** (id=6):
* Site: localhost:18000
* Slug: **default**
* Entity ID: https://saml.example.com/entityid
* Enabled: True
**SAMLProviderConfig** (id=11):
* Name: Test Localhost
* Slug: default
* Entity ID: https://saml.example.com/entityid
* Metadata Source: https://mocksaml.com/api/saml/metadata
* Backend Name: tpa-saml
* Site: localhost:18000
* SAML Configuration: → SAMLConfiguration (id=6)
* Enabled: True
**SAMLProviderData** (id=3):
* Entity ID: https://saml.example.com/entityid
* SSO URL: https://mocksaml.com/api/saml/sso
* Public Key: (certificate from MockSAML metadata)
* Fetched At: 2026-02-27 18:05:40+00:00
* Expires At: 2027-02-27 18:05:41+00:00
* Valid: True
**MockSAML Configuration**:
* SP Entity ID: https://saml.example.com/entityid
* ACS URL: http://localhost:18000/auth/complete/tpa-saml/
* Test User Attributes: email, firstName, lastName, uid

View File

@@ -0,0 +1,26 @@
# Generated migration for adding optional checkbox skip configuration field
from django.db import migrations, models
import django.utils.translation
class Migration(migrations.Migration):
dependencies = [
('third_party_auth', '0013_default_site_id_wrapper_function'),
]
operations = [
migrations.AddField(
model_name='samlproviderconfig',
name='skip_registration_optional_checkboxes',
field=models.BooleanField(
default=False,
help_text=django.utils.translation.gettext_lazy(
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
"on the registration form for users registering via this provider. When these checkboxes "
"are skipped, their values are inferred as False (opted out)."
),
),
),
]

View File

@@ -745,6 +745,14 @@ class SAMLProviderConfig(ProviderConfig):
"immediately after authenticating with the third party instead of the login page."
),
)
skip_registration_optional_checkboxes = models.BooleanField(
default=False,
help_text=_(
"If enabled, optional checkboxes (marketing emails opt-in, etc.) will not be rendered "
"on the registration form for users registering via this provider. When these checkboxes "
"are skipped, their values are inferred as False (opted out)."
),
)
other_settings = models.TextField(
verbose_name="Advanced settings", blank=True,
help_text=(

View File

@@ -58,6 +58,7 @@
);
this.currentProvider = data.thirdPartyAuth.currentProvider || '';
this.syncLearnerProfileData = data.thirdPartyAuth.syncLearnerProfileData || false;
this.skipRegistrationOptionalCheckboxes = data.thirdPartyAuth.skipRegistrationOptionalCheckboxes || false;
this.errorMessage = data.thirdPartyAuth.errorMessage || '';
this.platformName = data.platformName;
this.autoSubmit = data.thirdPartyAuth.autoSubmitRegForm;
@@ -156,6 +157,7 @@
fields: fields,
currentProvider: this.currentProvider,
syncLearnerProfileData: this.syncLearnerProfileData,
skipRegistrationOptionalCheckboxes: this.skipRegistrationOptionalCheckboxes,
providers: this.providers,
hasSecondaryProviders: this.hasSecondaryProviders,
platformName: this.platformName,

View File

@@ -56,14 +56,16 @@
<div class="form-fields <% if (context.is_require_third_party_auth_enabled) { %>hidden<% } %>">
<%= context.fields /* xss-lint: disable=underscore-not-escaped */ %>
<div class="form-field checkbox-optional_fields_toggle">
<input type="checkbox" id="toggle_optional_fields" class="input-block checkbox"">
<label for="toggle_optional_fields">
<span class="label-text-small">
<%- gettext("Support education research by providing additional information") %>
</span>
</label>
</div>
<% if (!context.skipRegistrationOptionalCheckboxes) { %>
<div class="form-field checkbox-optional_fields_toggle">
<input type="checkbox" id="toggle_optional_fields" class="input-block checkbox"">
<label for="toggle_optional_fields">
<span class="label-text-small">
<%- gettext("Support education research by providing additional information") %>
</span>
</label>
</div>
<% } %>
<button type="submit" class="action action-primary action-update js-register register-button">
<% if ( context.registerFormSubmitButtonText ) { %><%- context.registerFormSubmitButtonText %><% } else { %><%- gettext("Create Account") %><% } %>

View File

@@ -3,6 +3,7 @@ Objects and utilities used to construct registration forms.
"""
import copy
import logging
import re
from importlib import import_module
@@ -18,6 +19,7 @@ from django_countries import countries
from eventtracking import tracker
from common.djangoapps import third_party_auth
from common.djangoapps.third_party_auth.models import SAMLProviderConfig
from common.djangoapps.edxmako.shortcuts import marketing_link
from common.djangoapps.student.models import CourseEnrollmentAllowed, UserProfile, email_exists_or_retired
from common.djangoapps.util.password_policy_validators import (
@@ -36,6 +38,9 @@ from openedx.core.djangolib.markup import HTML, Text
from openedx.features.enterprise_support.api import enterprise_customer_for_request
log = logging.getLogger(__name__)
class TrueCheckbox(widgets.CheckboxInput):
"""
A checkbox widget that only accepts "true" (case-insensitive) as true.
@@ -334,7 +339,20 @@ class RegistrationFormFactory:
def _is_field_visible(self, field_name):
"""Check whether a field is visible based on Django settings. """
return self._extra_fields_setting.get(field_name) in ["required", "optional", "optional-exposed"]
is_visible = self._extra_fields_setting.get(field_name) in ["required", "optional", "optional-exposed"]
# If SAML provider config wants to skip optional checkboxes, hide marketing_emails_opt_in
if is_visible and field_name == 'marketing_emails_opt_in':
saml_config = self._get_saml_provider_config()
if saml_config and saml_config.skip_registration_optional_checkboxes:
log.info(
"SAML provider %s has skip_registration_optional_checkboxes=True, "
"hiding marketing_emails_opt_in field",
saml_config.slug
)
return False
return is_visible
def _is_field_required(self, field_name):
"""Check whether a field is required based on Django settings. """
@@ -410,6 +428,62 @@ class RegistrationFormFactory:
field_order.extend(sorted(difference))
self.field_order = field_order
self.request = None # Will be set by get_registration_form
def _get_saml_provider_config(self):
"""
Get the SAML provider config for the current request's running pipeline.
Returns:
SAMLProviderConfig or None: The SAML provider config if found, None otherwise
"""
if not self.request or not third_party_auth.is_enabled():
return None
running_pipeline = third_party_auth.pipeline.get(self.request)
if not running_pipeline:
return None
try:
# idp_name can be in kwargs directly, in kwargs['details'], or in kwargs['response']
saml_provider_name = running_pipeline.get('kwargs', {}).get('idp_name')
if not saml_provider_name:
saml_provider_name = (
running_pipeline.get('kwargs', {})
.get('details', {})
.get('idp_name')
)
if not saml_provider_name:
saml_provider_name = (
running_pipeline.get('kwargs', {})
.get('response', {})
.get('idp_name')
)
if not saml_provider_name:
return None
try:
# Try to find the SAML provider config
# First try with current_set(), then fall back to direct query
try:
return SAMLProviderConfig.objects.current_set().get(
slug=saml_provider_name
)
except SAMLProviderConfig.DoesNotExist:
# Fallback to direct query without current_set()
return SAMLProviderConfig.objects.get(
slug=saml_provider_name
)
except SAMLProviderConfig.DoesNotExist:
log.debug(
"SAML provider config not found for idp_name: %s",
saml_provider_name
)
return None
except Exception as exc: # pylint: disable=broad-except
log.debug("Error getting SAML provider config: %s", str(exc))
return None
def get_registration_form(self, request):
"""Return a description of the registration form.
@@ -426,6 +500,7 @@ class RegistrationFormFactory:
Returns:
HttpResponse
"""
self.request = request
form_desc = FormDescription("post", self._get_registration_submit_url(request))
self._apply_third_party_auth_overrides(request, form_desc)
@@ -693,6 +768,11 @@ class RegistrationFormFactory:
def _add_marketing_emails_opt_in_field(self, form_desc, required=False):
"""Add a marketing email checkbox to form description.
If a SAML provider config has skip_registration_optional_checkboxes=True,
the field will default to False (opt-out) and not be required, overriding
the global settings.
Arguments:
form_desc: A form description
Keyword Arguments:
@@ -703,13 +783,31 @@ class RegistrationFormFactory:
platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
)
# Default: checkbox is checked, field requirement follows the passed parameter
default_value = True
field_required = required
field_exposed = True
# Check if SAML provider wants to skip optional checkboxes
# This overrides both global settings and provider field overrides
saml_config = self._get_saml_provider_config()
if saml_config and saml_config.skip_registration_optional_checkboxes:
log.info(
"SAML provider %s has skip_registration_optional_checkboxes=True, "
"hiding field and setting default to False",
saml_config.slug
)
default_value = False # User opts out by default when field is skipped
field_required = False # Make field optional
field_exposed = False # Hide the field from the form
form_desc.add_field(
'marketing_emails_opt_in',
label=opt_in_label,
field_type="checkbox",
exposed=True,
default=True, # the checkbox will automatically be checked; meaning user has opted in
required=required,
exposed=field_exposed,
default=default_value,
required=field_required,
)
def _add_field_with_configurable_select_options(self, field_name, field_label, form_desc, required=False):
@@ -1149,7 +1247,24 @@ class RegistrationFormFactory:
)
for field_name in self.DEFAULT_FIELDS + self.EXTRA_FIELDS:
if field_name in field_overrides:
if field_name not in field_overrides:
continue
# Special handling for marketing_emails_opt_in:
# If SAML provider config has skip_registration_optional_checkboxes=True,
# don't let the provider's get_register_form_data override the default
skip_override = False
if field_name == 'marketing_emails_opt_in':
saml_config = self._get_saml_provider_config()
if saml_config and saml_config.skip_registration_optional_checkboxes:
log.debug(
"Skipping provider override for marketing_emails_opt_in "
"due to SAML config for provider: %s",
saml_config.slug
)
skip_override = True
if not skip_override:
form_desc.override_field_properties(
field_name, default=field_overrides[field_name]
)
@@ -1159,9 +1274,11 @@ class RegistrationFormFactory:
field_overrides[field_name] and
hide_registration_fields_except_tos
):
field_default = field_overrides[field_name]
form_desc.override_field_properties(
field_name,
field_type="hidden",
default=field_default,
label="",
instructions="",
)

View File

@@ -569,7 +569,8 @@ class LoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleSto
"errorMessage": None,
"registerFormSubmitButtonText": "Create Account",
"syncLearnerProfileData": False,
"pipeline_user_details": {"email": "test@test.com"} if add_user_details else {}
"pipeline_user_details": {"email": "test@test.com"} if add_user_details else {},
"skipRegistrationOptionalCheckboxes": False
}
if expected_ec is not None:
# If we set an EnterpriseCustomer, third-party auth providers ought to be hidden.
@@ -600,7 +601,8 @@ class LoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleSto
'errorMessage': expected_error_message,
'registerFormSubmitButtonText': 'Create Account',
'syncLearnerProfileData': False,
'pipeline_user_details': {'response': {'idp_name': 'testshib'}}
'pipeline_user_details': {'response': {'idp_name': 'testshib'}},
'skipRegistrationOptionalCheckboxes': False
}
auth_info = dump_js_escaped_json(auth_info)

View File

@@ -0,0 +1,318 @@
"""
Tests for SAML provider configuration to skip optional checkboxes in registration form.
"""
import logging
from unittest import mock
from django.test import TestCase, override_settings
from django.test.client import RequestFactory
from common.djangoapps.third_party_auth.tests.factories import SAMLProviderConfigFactory
from common.djangoapps.third_party_auth.tests.testutil import simulate_running_pipeline
from openedx.core.djangoapps.user_authn.views.registration_form import RegistrationFormFactory
log = logging.getLogger(__name__)
class SAMLProviderOptionalCheckboxTest(TestCase):
"""
Tests for SAML provider configuration options to skip optional checkboxes
(marketing emails, etc.) during registration.
"""
def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.factory = RequestFactory()
def _create_request(self):
"""Create a test request with session support."""
from importlib import import_module
from django.conf import settings
request = self.factory.get('/register')
engine = import_module(settings.SESSION_ENGINE)
session_key = None
request.session = engine.SessionStore(session_key)
return request
@override_settings(
MARKETING_EMAILS_OPT_IN=True,
REGISTRATION_EXTRA_FIELDS={},
REGISTRATION_FIELD_ORDER=[]
)
@mock.patch(
'openedx.core.djangoapps.user_authn.views.registration_form.third_party_auth.is_enabled',
return_value=True,
)
def test_marketing_checkbox_hidden_with_marketing_opt_in_setting(self, mock_is_enabled):
"""
Test that marketing checkbox is hidden when SAML provider config
has skip_registration_optional_checkboxes=True, even when the global
MARKETING_EMAILS_OPT_IN setting is True (production scenario).
"""
# Create a SAML provider config that skips optional checkboxes
saml_config = SAMLProviderConfigFactory(
skip_registration_optional_checkboxes=True
)
# Simulate running SAML authentication pipeline
with simulate_running_pipeline(
"common.djangoapps.third_party_auth.pipeline",
"tpa-saml",
idp_name=saml_config.slug,
email="testuser@example.com",
fullname="Test User",
username="testuser"
):
request = self._create_request()
form_factory = RegistrationFormFactory()
form_desc = form_factory.get_registration_form(request)
# Find the marketing_emails_opt_in field
marketing_field = None
for field in form_desc.fields:
if field['name'] == 'marketing_emails_opt_in':
marketing_field = field
break
# Even though MARKETING_EMAILS_OPT_IN=True globally,
# the field should not be present when skipped via SAML config
self.assertIsNone(
marketing_field,
"marketing_emails_opt_in field should not be present when skipped via SAML config, "
"even when MARKETING_EMAILS_OPT_IN=True"
)
@override_settings(
MARKETING_EMAILS_OPT_IN=True,
REGISTRATION_EXTRA_FIELDS={},
REGISTRATION_FIELD_ORDER=[]
)
@mock.patch(
'openedx.core.djangoapps.user_authn.views.registration_form.third_party_auth.is_enabled',
return_value=True,
)
def test_marketing_checkbox_visible_with_marketing_opt_in_setting_no_skip(self, mock_is_enabled):
"""
Test that marketing checkbox is visible when MARKETING_EMAILS_OPT_IN=True
and SAML provider config does NOT have skip_registration_optional_checkboxes=True.
"""
# Create a SAML provider config that doesn't skip checkboxes
saml_config = SAMLProviderConfigFactory(
skip_registration_optional_checkboxes=False
)
# Simulate running SAML authentication pipeline
with simulate_running_pipeline(
"common.djangoapps.third_party_auth.pipeline",
"tpa-saml",
idp_name=saml_config.slug,
email="testuser@example.com",
fullname="Test User",
username="testuser"
):
request = self._create_request()
form_factory = RegistrationFormFactory()
form_desc = form_factory.get_registration_form(request)
# Find the marketing_emails_opt_in field
marketing_field = None
for field in form_desc.fields:
if field['name'] == 'marketing_emails_opt_in':
marketing_field = field
break
# When MARKETING_EMAILS_OPT_IN=True and SAML config doesn't skip,
# the field should be present
self.assertIsNotNone(
marketing_field,
"marketing_emails_opt_in field should be present when MARKETING_EMAILS_OPT_IN=True "
"and SAML config does not skip checkboxes"
)
# The field should be visible (exposed)
self.assertTrue(
marketing_field.get('exposed', False),
"Marketing checkbox should be visible when SAML config does not skip checkboxes"
)
# The field should be optional (not required) when MARKETING_EMAILS_OPT_IN=True
self.assertFalse(
marketing_field.get('required', False),
"Marketing checkbox should be optional when MARKETING_EMAILS_OPT_IN=True"
)
@override_settings(
REGISTRATION_EXTRA_FIELDS={
"marketing_emails_opt_in": "optional"
},
REGISTRATION_FIELD_ORDER=[]
)
def test_marketing_checkbox_optional_without_saml_config(self):
"""
Test that marketing checkbox is optional by default when REGISTRATION_EXTRA_FIELDS
is set to optional, regardless of SAML config.
"""
request = self._create_request()
form_factory = RegistrationFormFactory()
form_desc = form_factory.get_registration_form(request)
# Find the marketing_emails_opt_in field
marketing_field = None
for field in form_desc.fields:
if field['name'] == 'marketing_emails_opt_in':
marketing_field = field
break
self.assertIsNotNone(marketing_field, "marketing_emails_opt_in field not found")
# When REGISTRATION_EXTRA_FIELDS is optional, the field should not be required
self.assertFalse(marketing_field.get('required', False))
# The field should be visible (exposed=True) by default
self.assertTrue(
marketing_field.get('exposed', False),
"Marketing checkbox should be visible when no SAML config skips it"
)
@override_settings(
REGISTRATION_EXTRA_FIELDS={
"marketing_emails_opt_in": "required"
},
REGISTRATION_FIELD_ORDER=[]
)
@mock.patch(
'openedx.core.djangoapps.user_authn.views.registration_form.third_party_auth.is_enabled',
return_value=True,
)
def test_marketing_checkbox_optional_with_saml_config(self, mock_is_enabled):
"""
Test that marketing checkbox is hidden when SAML provider config
has skip_registration_optional_checkboxes=True, overriding global settings.
"""
# Create a SAML provider config that skips optional checkboxes
saml_config = SAMLProviderConfigFactory(
skip_registration_optional_checkboxes=True
)
# Simulate running SAML authentication pipeline
with simulate_running_pipeline(
"common.djangoapps.third_party_auth.pipeline",
"tpa-saml",
idp_name=saml_config.slug,
email="testuser@example.com",
fullname="Test User",
username="testuser"
):
request = self._create_request()
form_factory = RegistrationFormFactory()
form_desc = form_factory.get_registration_form(request)
# Find the marketing_emails_opt_in field
marketing_field = None
for field in form_desc.fields:
if field['name'] == 'marketing_emails_opt_in':
marketing_field = field
break
# When SAML provider config sets skip_registration_optional_checkboxes=True,
# the field should not be present in the form at all
self.assertIsNone(
marketing_field,
"marketing_emails_opt_in field should not be present when skipped via SAML config"
)
@override_settings(
REGISTRATION_EXTRA_FIELDS={
"marketing_emails_opt_in": "required"
},
REGISTRATION_FIELD_ORDER=[]
)
@mock.patch(
'openedx.core.djangoapps.user_authn.views.registration_form.third_party_auth.is_enabled',
return_value=True,
)
def test_marketing_checkbox_still_optional_when_config_false(self, mock_is_enabled):
"""
Test that when SAML provider config has skip_registration_optional_checkboxes=False,
the global REGISTRATION_EXTRA_FIELDS setting is used (required in this case).
"""
# Create a SAML provider config that doesn't skip checkboxes (default behavior)
saml_config = SAMLProviderConfigFactory(
skip_registration_optional_checkboxes=False
)
# Simulate running SAML authentication pipeline
with simulate_running_pipeline(
"common.djangoapps.third_party_auth.pipeline",
"tpa-saml",
idp_name=saml_config.slug,
email="testuser@example.com",
fullname="Test User",
username="testuser"
):
request = self._create_request()
form_factory = RegistrationFormFactory()
form_desc = form_factory.get_registration_form(request)
# Find the marketing_emails_opt_in field
marketing_field = None
for field in form_desc.fields:
if field['name'] == 'marketing_emails_opt_in':
marketing_field = field
break
self.assertIsNotNone(marketing_field, "marketing_emails_opt_in field not found")
# When SAML provider config sets skip_registration_optional_checkboxes=False,
# it should use the global setting (required in this test)
self.assertTrue(marketing_field.get('required', False))
# The field should be visible (exposed=True) when config is False
self.assertTrue(
marketing_field.get('exposed', False),
"Marketing checkbox should be visible when SAML config is False"
)
@override_settings(
REGISTRATION_EXTRA_FIELDS={
"marketing_emails_opt_in": "required"
},
REGISTRATION_FIELD_ORDER=[]
)
@mock.patch(
'openedx.core.djangoapps.user_authn.views.registration_form.third_party_auth.is_enabled',
return_value=True,
)
def test_marketing_checkbox_hidden_with_saml_config(self, mock_is_enabled):
"""
Test that when marketing checkbox is skipped via SAML provider config,
it is not present in the form at all (completely hidden).
"""
# Create a SAML provider config that skips optional checkboxes
saml_config = SAMLProviderConfigFactory(
skip_registration_optional_checkboxes=True
)
# Simulate running SAML authentication pipeline
with simulate_running_pipeline(
"common.djangoapps.third_party_auth.pipeline",
"tpa-saml",
idp_name=saml_config.slug,
email="testuser@example.com",
fullname="Test User",
username="testuser"
):
request = self._create_request()
form_factory = RegistrationFormFactory()
form_desc = form_factory.get_registration_form(request)
# Find the marketing_emails_opt_in field
marketing_field = None
for field in form_desc.fields:
if field['name'] == 'marketing_emails_opt_in':
marketing_field = field
break
# When SAML provider config sets skip_registration_optional_checkboxes=True,
# the field should not be present in the form at all
self.assertIsNone(
marketing_field,
"marketing_emails_opt_in field should not be present when skipped via SAML config"
)

View File

@@ -52,7 +52,8 @@ def third_party_auth_context(request, redirect_to, tpa_hint=None):
"errorMessage": None,
"registerFormSubmitButtonText": _("Create Account"),
"syncLearnerProfileData": False,
"pipeline_user_details": {}
"pipeline_user_details": {},
"skipRegistrationOptionalCheckboxes": False
}
if third_party_auth.is_enabled():
@@ -96,6 +97,12 @@ def third_party_auth_context(request, redirect_to, tpa_hint=None):
# As a reliable way of "skipping" the registration form, we just submit it automatically
context["autoSubmitRegForm"] = True
# Check if SAML provider wants to skip optional checkboxes
if hasattr(current_provider, 'skip_registration_optional_checkboxes'):
context["skipRegistrationOptionalCheckboxes"] = (
current_provider.skip_registration_optional_checkboxes
)
# Check for any error messages we may want to display:
for msg in messages.get_messages(request):
if msg.extra_tags.split()[0] == "social-auth":