Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3695,6 +3695,18 @@ def _should_send_certificate_events(settings):
# The project ID should be obtained from the Google Cloud Console when creating a reCAPTCHA
RECAPTCHA_PROJECT_ID = None

# .. setting_name: OPEN_EDX_FILTERS_CONFIG
# .. setting_default: {}
# .. setting_description: Configuration dict for openedx-filters pipeline steps.
# Keys are filter type strings; values are dicts with 'fail_silently' (bool) and
# 'pipeline' (list of dotted-path strings to PipelineStep subclasses).
OPEN_EDX_FILTERS_CONFIG = {
"org.openedx.learning.account.settings.read_only_fields.requested.v1": {
"fail_silently": True,
Comment on lines +3698 to +3705
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings metadata comment says setting_default: {}, but this setting is being given a non-empty default value below. Update the setting_default to reflect the actual default config (or make the default {} if that's intended) so generated settings documentation stays accurate.

Copilot uses AI. Check for mistakes.
"pipeline": ["enterprise.filters.accounts.AccountSettingsReadOnlyFieldsStep"],
},
}

############################## Miscellaneous ###############################

# To limit the number of courses displayed on learner dashboard
Expand Down
14 changes: 14 additions & 0 deletions lms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def get_env_setting(setting):
'EVENT_BUS_PRODUCER_CONFIG',
'DEFAULT_FILE_STORAGE',
'STATICFILES_STORAGE',
'OPEN_EDX_FILTERS_CONFIG',
]
})

Expand Down Expand Up @@ -281,6 +282,19 @@ def get_env_setting(setting):
EVENT_TRACKING_SEGMENTIO_EMIT_WHITELIST
)

# Merge OPEN_EDX_FILTERS_CONFIG from YAML into the default defined in common.py.
# Pipeline steps from YAML are appended after steps defined in common.py.
# The fail_silently value from YAML takes precedence over the one in common.py.
for _filter_type, _filter_config in _YAML_TOKENS.get('OPEN_EDX_FILTERS_CONFIG', {}).items():
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_YAML_TOKENS.get('OPEN_EDX_FILTERS_CONFIG', {}) will return None if the YAML explicitly sets OPEN_EDX_FILTERS_CONFIG: null, which would raise an AttributeError on .items(). Consider using (_YAML_TOKENS.get('OPEN_EDX_FILTERS_CONFIG') or {}) (and ideally validating it’s a dict) before iterating to avoid startup-time crashes from a misconfigured YAML.

Suggested change
for _filter_type, _filter_config in _YAML_TOKENS.get('OPEN_EDX_FILTERS_CONFIG', {}).items():
_open_edx_filters_config = _YAML_TOKENS.get('OPEN_EDX_FILTERS_CONFIG') or {}
if not isinstance(_open_edx_filters_config, dict):
raise ImproperlyConfigured('OPEN_EDX_FILTERS_CONFIG must be a dict')
for _filter_type, _filter_config in _open_edx_filters_config.items():

Copilot uses AI. Check for mistakes.
if _filter_type in OPEN_EDX_FILTERS_CONFIG:
OPEN_EDX_FILTERS_CONFIG[_filter_type]['pipeline'].extend(
_filter_config.get('pipeline', [])
)
if 'fail_silently' in _filter_config:
OPEN_EDX_FILTERS_CONFIG[_filter_type]['fail_silently'] = _filter_config['fail_silently']
else:
OPEN_EDX_FILTERS_CONFIG[_filter_type] = _filter_config

if ENABLE_THIRD_PARTY_AUTH:
AUTHENTICATION_BACKENDS = _YAML_TOKENS.get('THIRD_PARTY_AUTH_BACKENDS', [
'social_core.backends.google.GoogleOAuth2',
Expand Down
10 changes: 8 additions & 2 deletions openedx/core/djangoapps/user_api/accounts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
from openedx.core.djangoapps.user_authn.utils import check_pwned_password
from openedx.core.djangoapps.user_authn.views.registration_form import validate_name, validate_username
from openedx.core.lib.api.view_utils import add_serializer_errors
from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields
from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed
from openedx_filters.learning.filters import AccountSettingsReadOnlyFieldsRequested

from .serializers import AccountLegacyProfileSerializer, AccountUserSerializer, UserReadOnlySerializer, _visible_fields

Expand Down Expand Up @@ -193,11 +193,17 @@ def update_account_settings(requesting_user, update, username=None):

def _validate_read_only_fields(user, data, field_errors):
# Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
plugin_readonly_fields, __ = AccountSettingsReadOnlyFieldsRequested.run_filter(
readonly_fields=set(),
user=user,
)
Comment on lines +196 to +199
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The openedx-filters call site is missing the standard # .. filter_implemented_name: / # .. filter_type: annotations used elsewhere in the codebase for filter invocations, which are relied on for documentation/discovery. Add these two comment lines immediately above the run_filter call (see e.g. openedx/core/djangoapps/user_authn/views/login.py around the StudentLoginRequested.run_filter usage).

Copilot uses AI. Check for mistakes.
plugin_readonly_fields = plugin_readonly_fields or set()

read_only_fields = set(data.keys()).intersection(
# Remove email since it is handled separately below when checking for changing_email.
(set(AccountUserSerializer.get_read_only_fields()) - {"email"}) |
set(AccountLegacyProfileSerializer.get_read_only_fields() or set()) |
get_enterprise_readonly_account_fields(user)
plugin_readonly_fields
)

for read_only_field in read_only_fields:
Expand Down
90 changes: 18 additions & 72 deletions openedx/core/djangoapps/user_api/accounts/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

import datetime
import itertools
import unicodedata
from unittest.mock import Mock, patch

Expand All @@ -18,7 +17,6 @@
from django.test.client import RequestFactory
from django.urls import reverse
from pytz import UTC
from social_django.models import UserSocialAuth

from common.djangoapps.student.models import (
AccountRecovery,
Expand Down Expand Up @@ -104,10 +102,12 @@ def setUp(self):
self.staff_user = UserFactory(is_staff=True, password=self.password)
self.reset_tracker()

enterprise_patcher = patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
enterprise_learner_patcher = enterprise_patcher.start()
enterprise_learner_patcher.return_value = {}
self.addCleanup(enterprise_learner_patcher.stop)
filter_patcher = patch(
'openedx.core.djangoapps.user_api.accounts.api.AccountSettingsReadOnlyFieldsRequested.run_filter',
return_value=(set(), None),
)
filter_patcher.start()
self.addCleanup(filter_patcher.stop)

def test_get_username_provided(self):
"""Test the difference in behavior when a username is supplied to get_account_settings."""
Expand Down Expand Up @@ -248,73 +248,19 @@ def test_update_success_for_enterprise(self):
account_settings = get_account_settings(self.default_request)[0]
assert level_of_education == account_settings['level_of_education']

@patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
@patch('openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get')
@ddt.data(
*itertools.product(
# field_name_value values
(("email", "new_email@example.com"), ("name", "new name"), ("country", "IN")),
# is_enterprise_user
(True, False),
# is_synch_learner_profile_data
(True, False),
# has `UserSocialAuth` record
(True, False),
)
@patch(
'openedx.core.djangoapps.user_api.accounts.api.AccountSettingsReadOnlyFieldsRequested.run_filter',
return_value=({'country'}, None),
)
@ddt.unpack
def test_update_validation_error_for_enterprise(
self,
field_name_value,
is_enterprise_user,
is_synch_learner_profile_data,
has_user_social_auth_record,
mock_auth_provider,
mock_customer,
):
idp_backend_name = 'tpa-saml'
mock_customer.return_value = {}
if is_enterprise_user:
mock_customer.return_value.update({
'uuid': 'real-ent-uuid',
'name': 'Dummy Enterprise',
'identity_provider': 'saml-ubc',
'identity_providers': [
{
"provider_id": "saml-ubc",
}
],
})
mock_auth_provider.return_value.sync_learner_profile_data = is_synch_learner_profile_data
mock_auth_provider.return_value.backend_name = idp_backend_name

update_data = {field_name_value[0]: field_name_value[1]}

user_fullname_editable = False
if has_user_social_auth_record:
UserSocialAuth.objects.create(
provider=idp_backend_name,
user=self.user
)
else:
UserSocialAuth.objects.all().delete()
# user's fullname is editable if no `UserSocialAuth` record exists
user_fullname_editable = field_name_value[0] == 'name'

# prevent actual email change requests
with patch('openedx.core.djangoapps.user_api.accounts.api.student_views.do_email_change_request'):
# expect field un-editability only when all of the following conditions are met
if is_enterprise_user and is_synch_learner_profile_data and not user_fullname_editable:
with pytest.raises(AccountValidationError) as validation_error:
update_account_settings(self.user, update_data)
field_errors = validation_error.value.field_errors
assert 'This field is not editable via this API' == \
field_errors[field_name_value[0]]['developer_message']
else:
update_account_settings(self.user, update_data)
account_settings = get_account_settings(self.default_request)[0]
if field_name_value[0] != "email":
assert field_name_value[1] == account_settings[field_name_value[0]]
def test_readonly_field_from_filter_is_rejected(self, mock_run_filter): # pylint: disable=unused-argument
"""
When AccountSettingsReadOnlyFieldsRequested.run_filter returns a field as read-only,
update_account_settings should raise AccountValidationError for that field.
"""
with pytest.raises(AccountValidationError) as exc_info:
update_account_settings(self.user, {"country": "IN"})
field_errors = exc_info.value.field_errors
assert 'This field is not editable via this API' == field_errors['country']['developer_message']

def test_update_error_validating(self):
"""Test that AccountValidationError is thrown if incorrect values are supplied."""
Expand Down
32 changes: 16 additions & 16 deletions requirements/edx-sandbox/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ cffi==2.0.0
# via cryptography
chem==2.0.0
# via -r requirements/edx-sandbox/base.in
click==8.3.0
click==8.3.2
# via nltk
codejail-includes==2.0.0
# via -r requirements/edx-sandbox/base.in
Expand All @@ -20,31 +20,31 @@ cryptography==45.0.7
# -r requirements/edx-sandbox/base.in
cycler==0.12.1
# via matplotlib
fonttools==4.60.1
fonttools==4.62.1
# via matplotlib
joblib==1.5.2
joblib==1.5.3
# via nltk
kiwisolver==1.4.9
kiwisolver==1.5.0
# via matplotlib
lxml[html-clean]==5.3.2
# via
# -c requirements/constraints.txt
# -r requirements/edx-sandbox/base.in
# lxml-html-clean
# openedx-calc
lxml-html-clean==0.4.3
lxml-html-clean==0.4.4
# via lxml
markupsafe==3.0.3
# via
# chem
# openedx-calc
matplotlib==3.10.6
matplotlib==3.10.8
# via -r requirements/edx-sandbox/base.in
mpmath==1.3.0
# via sympy
networkx==3.5
networkx==3.6.1
# via -r requirements/edx-sandbox/base.in
nltk==3.9.2
nltk==3.9.4
# via
# -r requirements/edx-sandbox/base.in
# chem
Expand All @@ -56,15 +56,15 @@ numpy==1.26.4
# matplotlib
# openedx-calc
# scipy
openedx-calc==4.0.2
openedx-calc==4.0.3
# via -r requirements/edx-sandbox/base.in
packaging==25.0
packaging==26.0
# via matplotlib
pillow==11.3.0
pillow==12.2.0
# via matplotlib
pycparser==2.23
pycparser==3.0
# via cffi
pyparsing==3.2.5
pyparsing==3.3.2
# via
# -r requirements/edx-sandbox/base.in
# chem
Expand All @@ -74,9 +74,9 @@ python-dateutil==2.9.0.post0
# via matplotlib
random2==1.0.2
# via -r requirements/edx-sandbox/base.in
regex==2025.9.18
regex==2026.4.4
# via nltk
scipy==1.16.2
scipy==1.17.1
# via
# -r requirements/edx-sandbox/base.in
# chem
Expand All @@ -86,5 +86,5 @@ sympy==1.14.0
# via
# -r requirements/edx-sandbox/base.in
# openedx-calc
tqdm==4.67.1
tqdm==4.67.3
# via nltk
4 changes: 2 additions & 2 deletions requirements/edx/assets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
#
# make upgrade
#
click==8.3.0
click==8.3.2
# via -r requirements/edx/assets.in
libsass==0.10.0
# via
# -c requirements/constraints.txt
# -r requirements/edx/assets.in
nodeenv==1.9.1
nodeenv==1.10.0
# via -r requirements/edx/assets.in
six==1.17.0
# via libsass
Loading
Loading