diff --git a/cms/envs/common.py b/cms/envs/common.py
index e2a7b375b839..b85fa90c0229 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -2284,7 +2284,6 @@
############################ OAUTH2 Provider ###################################
-
# 5 minute expiration time for JWT id tokens issued for external API requests.
OAUTH_ID_TOKEN_EXPIRATION = 5 * 60
@@ -2300,6 +2299,12 @@
API_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/'
AUTH_DOCUMENTATION_URL = 'https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/index.html'
+EDX_DRF_EXTENSIONS = {
+ # Set this value to an empty dict in order to prevent automatically updating
+ # user data from values in (possibly stale) JWTs.
+ 'JWT_PAYLOAD_USER_ATTRIBUTE_MAPPING': {},
+}
+
############## Settings for Studio Context Sensitive Help ##############
HELP_TOKENS_INI_FILE = REPO_ROOT / "cms" / "envs" / "help_tokens.ini"
diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py
index 4d99b7d7722d..eddddbb88717 100644
--- a/lms/djangoapps/certificates/views/webview.py
+++ b/lms/djangoapps/certificates/views/webview.py
@@ -458,14 +458,15 @@ def _update_organization_context(context, course):
Updates context with organization related info.
"""
partner_long_name, organization_logo = None, None
- partner_short_name = course.display_organization if course.display_organization else course.org
+ course_org_display = course.display_organization
organizations = organizations_api.get_course_organizations(course_key=course.id)
if organizations:
# TODO Need to add support for multiple organizations, Currently we are interested in the first one.
organization = organizations[0]
partner_long_name = organization.get('name', partner_long_name)
- partner_short_name = organization.get('short_name', partner_short_name)
+ course_org_display = course_org_display or organization.get('short_name')
organization_logo = organization.get('logo', None)
+ partner_short_name = course_org_display or course.org
context['organization_long_name'] = partner_long_name
context['organization_short_name'] = partner_short_name
diff --git a/lms/djangoapps/course_api/blocks/utils.py b/lms/djangoapps/course_api/blocks/utils.py
index 3a9fea117c23..0af24b951ade 100644
--- a/lms/djangoapps/course_api/blocks/utils.py
+++ b/lms/djangoapps/course_api/blocks/utils.py
@@ -1,6 +1,8 @@
"""
Utils for Blocks
"""
+from rest_framework.utils.serializer_helpers import ReturnList
+
from openedx.core.djangoapps.discussions.models import (
DiscussionsConfiguration,
Provider,
@@ -15,16 +17,28 @@ def filter_discussion_xblocks_from_response(response, course_key):
provider = configuration.provider_type
if provider == Provider.OPEN_EDX:
# Finding ids of discussion xblocks
- discussion_xblocks = [
- key for key, value in response.data.get('blocks', {}).items()
- if value.get('type') == 'discussion'
- ]
+ if isinstance(response.data, ReturnList):
+ discussion_xblocks = [
+ value.get('id') for value in response.data if value.get('type') == 'discussion'
+ ]
+ else:
+ discussion_xblocks = [
+ key for key, value in response.data.get('blocks', {}).items()
+ if value.get('type') == 'discussion'
+ ]
# Filtering discussion xblocks keys from blocks
- filtered_blocks = {
- key: value
- for key, value in response.data.get('blocks', {}).items()
- if value.get('type') != 'discussion'
- }
+ if isinstance(response.data, ReturnList):
+ filtered_blocks = {
+ value.get('id'): value
+ for value in response.data
+ if value.get('type') != 'discussion'
+ }
+ else:
+ filtered_blocks = {
+ key: value
+ for key, value in response.data.get('blocks', {}).items()
+ if value.get('type') != 'discussion'
+ }
# Removing reference of discussion xblocks from unit
# These references needs to be removed because they no longer exist
for _, block_data in filtered_blocks.items():
@@ -36,5 +50,8 @@ def filter_discussion_xblocks_from_response(response, course_key):
if descendant not in discussion_xblocks
]
block_data[key] = descendants
- response.data['blocks'] = filtered_blocks
+ if isinstance(response.data, ReturnList):
+ response.data = filtered_blocks
+ else:
+ response.data['blocks'] = filtered_blocks
return response
diff --git a/lms/djangoapps/course_home_api/progress/serializers.py b/lms/djangoapps/course_home_api/progress/serializers.py
index 190cd76bf9b5..6bdc204434af 100644
--- a/lms/djangoapps/course_home_api/progress/serializers.py
+++ b/lms/djangoapps/course_home_api/progress/serializers.py
@@ -145,3 +145,4 @@ class ProgressTabSerializer(VerifiedModeSerializer):
username = serializers.CharField()
user_has_passing_grade = serializers.BooleanField()
verification_data = VerificationDataSerializer()
+ disable_progress_graph = serializers.BooleanField()
diff --git a/lms/djangoapps/course_home_api/progress/views.py b/lms/djangoapps/course_home_api/progress/views.py
index 8c21335dcacf..3783c19061dc 100644
--- a/lms/djangoapps/course_home_api/progress/views.py
+++ b/lms/djangoapps/course_home_api/progress/views.py
@@ -230,6 +230,7 @@ def get(self, request, *args, **kwargs):
block = modulestore().get_course(course_key)
grading_policy = block.grading_policy
+ disable_progress_graph = block.disable_progress_graph
verification_status = IDVerificationService.user_status(student)
verification_link = None
if verification_status['status'] is None or verification_status['status'] == 'expired':
@@ -259,6 +260,7 @@ def get(self, request, *args, **kwargs):
'username': username,
'user_has_passing_grade': user_has_passing_grade,
'verification_data': verification_data,
+ 'disable_progress_graph': disable_progress_graph,
}
context = self.get_serializer_context()
context['staff_access'] = is_staff
diff --git a/lms/static/js/instructor_dashboard/membership.js b/lms/static/js/instructor_dashboard/membership.js
index 5fdf4a42ef63..0cdb6d28330a 100644
--- a/lms/static/js/instructor_dashboard/membership.js
+++ b/lms/static/js/instructor_dashboard/membership.js
@@ -503,14 +503,20 @@ such that the value can be defined later than this assignment (file load order).
}));
$idsList = $('
');
$taskResSection.append($idsList);
- for (j = 0, len1 = ids.length; j < len1; j++) {
- identifier = ids[j];
- $idsList.append($('', {
- text: identifier
- }));
- }
+ if (ids && ids.length > 0) {
+ for (j = 0, len1 = ids.length; j < len1; j++) {
+ identifier = ids[j];
+ $idsList.append($('', {
+ text: identifier
+ }));
+ }
+ }
return displayResponse.$task_response.append($taskResSection);
};
+ if (errors.length === 0 && successes.length === 0 && noUsers.length === 0) {
+ // Translators: For cases when the input field is empty;
+ renderList(gettext('This field must not be blank'), []);
+ }
if (successes.length && dataFromServer.action === 'add') {
var j, len1, inActiveUsers, activeUsers; // eslint-disable-line vars-on-top
activeUsers = [];
@@ -585,6 +591,9 @@ such that the value can be defined later than this assignment (file load order).
sr = noUsers[j];
results.push(sr.identifier);
}
+ results.unshift(
+ gettext('Users must create and activate their account before they can be promoted to beta tester.')
+ );
return results;
}()));
}
@@ -699,14 +708,28 @@ such that the value can be defined later than this assignment (file load order).
}));
$idsList = $('');
$taskResSection.append($idsList);
- for (h = 0, len3 = ids.length; h < len3; h++) {
- identifier = ids[h];
- $idsList.append($('', {
- text: identifier
- }));
+ if (ids && ids.length > 0) {
+ for (h = 0, len3 = ids.length; h < len3; h++) {
+ identifier = ids[h];
+ $idsList.append($('', {
+ text: identifier
+ }));
+ }
}
return displayResponse.$task_response.append($taskResSection);
};
+ if (
+ invalidIdentifier.length === 0
+ && errors.length === 0
+ && enrolled.length === 0
+ && allowed.length === 0
+ && autoenrolled.length === 0
+ && notenrolled.length === 0
+ && notunenrolled.length === 0
+ ) {
+ // Translators: For cases when the input field is empty;
+ renderList(gettext('This field must not be blank'), []);
+ }
if (invalidIdentifier.length) {
renderList(gettext('The following email addresses and/or usernames are invalid:'), (function() {
var m, len4, results;
diff --git a/lms/static/sass/course/wiki/_wiki.scss b/lms/static/sass/course/wiki/_wiki.scss
index b3023af1d7bb..c896ff391868 100644
--- a/lms/static/sass/course/wiki/_wiki.scss
+++ b/lms/static/sass/course/wiki/_wiki.scss
@@ -262,9 +262,9 @@
a {
display: block;
padding: 2px 4px 2px 10px;
- border-radius: 3px;
font-size: 0.9em;
line-height: 25px;
+ border-left: 1px solid $m-blue;
&:hover,
&:focus {
diff --git a/lms/templates/wiki/article.html b/lms/templates/wiki/article.html
index 542512c762d0..1a38d01aeb6d 100644
--- a/lms/templates/wiki/article.html
+++ b/lms/templates/wiki/article.html
@@ -33,7 +33,7 @@ {{ article.current_revision.title }}
{% if urlpath %}
{% endif %}
diff --git a/openedx/core/djangoapps/credentials/tasks/v1/tasks.py b/openedx/core/djangoapps/credentials/tasks/v1/tasks.py
index dad4d3618a97..ae0a3da8ae92 100644
--- a/openedx/core/djangoapps/credentials/tasks/v1/tasks.py
+++ b/openedx/core/djangoapps/credentials/tasks/v1/tasks.py
@@ -373,7 +373,11 @@ def send_grade_if_interesting(
# Don't worry about whether it's available as well as awarded. Just awarded is good enough to record a verified
# attempt at a course. We want even the grades that didn't pass the class because Credentials wants to know about
# those too.
- if mode not in INTERESTING_MODES or status not in INTERESTING_STATUSES:
+ if (
+ mode not in INTERESTING_MODES
+ and not CourseMode.is_eligible_for_certificate(mode)
+ or status not in INTERESTING_STATUSES
+ ):
if verbose:
logger.info(f"Skipping send grade: mode/status uninteresting for mode [{mode}] & status [{status}]")
return
@@ -452,7 +456,10 @@ def backfill_date_for_all_course_runs():
course_key = str(course_run.id)
course_modes = CourseMode.objects.filter(course_id=course_key)
# There should only ever be one certificate relevant mode per course run
- modes = [mode.slug for mode in course_modes if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES]
+ modes = [
+ mode.slug for mode in course_modes
+ if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES or CourseMode.is_eligible_for_certificate(mode.slug)
+ ]
if len(modes) != 1:
logger.exception(
f'Either course {course_key} has no certificate mode or multiple modes. Task failed.'
@@ -503,7 +510,10 @@ def clean_certificate_available_date():
course_key = str(course_run.id)
course_modes = CourseMode.objects.filter(course_id=course_key)
# There should only ever be one certificate relevant mode per course run
- modes = [mode.slug for mode in course_modes if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES]
+ modes = [
+ mode.slug for mode in course_modes
+ if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES or CourseMode.is_eligible_for_certificate(mode.slug)
+ ]
if len(modes) != 1:
logger.exception(f'Either course {course_key} has no certificate mode or multiple modes. Task failed.')
# if there is only one relevant mode, post to credentials
diff --git a/openedx/core/djangoapps/programs/tasks.py b/openedx/core/djangoapps/programs/tasks.py
index a22bff876de4..0a23be073b50 100644
--- a/openedx/core/djangoapps/programs/tasks.py
+++ b/openedx/core/djangoapps/programs/tasks.py
@@ -384,7 +384,10 @@ def update_credentials_course_certificate_configuration_available_date(
course_key = str(course_key)
course_modes = CourseMode.objects.filter(course_id=course_key)
# There should only ever be one certificate relevant mode per course run
- modes = [mode.slug for mode in course_modes if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES]
+ modes = [
+ mode.slug for mode in course_modes
+ if mode.slug in CourseMode.CERTIFICATE_RELEVANT_MODES or CourseMode.is_eligible_for_certificate(mode.slug)
+ ]
if len(modes) != 1:
LOGGER.exception(
f'Either course {course_key} has no certificate mode or multiple modes. Task failed.'
@@ -471,7 +474,10 @@ def _retry_with_custom_exception(username, course_run_key, reason, countdown):
f"for {course_key} to user {username}"
)
return
- if certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES:
+ if (
+ certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES
+ or CourseMode.is_eligible_for_certificate(certificate.mode)
+ ):
try:
course_overview = CourseOverview.get_from_id(course_key)
except (CourseOverview.DoesNotExist, OSError):
diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_utils.py b/openedx/core/djangoapps/user_api/accounts/tests/test_utils.py
index 5755143e4a94..3b9293487361 100644
--- a/openedx/core/djangoapps/user_api/accounts/tests/test_utils.py
+++ b/openedx/core/djangoapps/user_api/accounts/tests/test_utils.py
@@ -43,7 +43,7 @@ def validate_social_link(self, social_platform, link):
('twitter', 'https://www.twiter.com/edX/', None, False),
('twitter', 'https://www.twitter.com/edX/123s', None, False),
('twitter', 'twitter.com/edX', 'https://www.twitter.com/edX', True),
- ('twitter', 'twitter.com/edX?foo=bar', 'https://www.twitter.com/edX', True),
+ ('twitter', 'twitter.com/edX?foo=bar', 'https://www.twitter.com/edX?foo=bar', True),
('twitter', 'twitter.com/test.user', 'https://www.twitter.com/test.user', True),
('linkedin', 'www.linkedin.com/harryrein', None, False),
('linkedin', 'www.linkedin.com/in/harryrein-1234', 'https://www.linkedin.com/in/harryrein-1234', True),
diff --git a/openedx/core/djangoapps/user_api/accounts/utils.py b/openedx/core/djangoapps/user_api/accounts/utils.py
index 1326c36a2eb5..13f8459cedd7 100644
--- a/openedx/core/djangoapps/user_api/accounts/utils.py
+++ b/openedx/core/djangoapps/user_api/accounts/utils.py
@@ -6,7 +6,6 @@
import random
import re
import string
-from urllib.parse import urlparse # pylint: disable=import-error
import waffle # lint-amnesty, pylint: disable=invalid-django-waffle-import
from completion.models import BlockCompletion
@@ -85,11 +84,8 @@ def _get_username_from_social_link(platform_name, new_social_link):
if not new_social_link:
return new_social_link
- # Parse the social link as if it were a URL.
- parse_result = urlparse(new_social_link)
- url_domain_and_path = parse_result[1] + parse_result[2]
url_stub = re.escape(settings.SOCIAL_PLATFORMS[platform_name]['url_stub'])
- username_match = re.search(r'(www\.)?' + url_stub + r'(?P.*?)[/]?$', url_domain_and_path, re.IGNORECASE)
+ username_match = re.search(r'(www\.)?' + url_stub + r'(?P.+?)(?:/)?$', new_social_link, re.IGNORECASE)
if username_match:
username = username_match.group('username')
else:
diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt
index 7313473a1675..4abc9ae22cb3 100644
--- a/requirements/common_constraints.txt
+++ b/requirements/common_constraints.txt
@@ -17,10 +17,19 @@
# using LTS django version
-
+Django<5.0
# elasticsearch>=7.14.0 includes breaking changes in it which caused issues in discovery upgrade process.
# elastic search changelog: https://www.elastic.co/guide/en/enterprise-search/master/release-notes-7.14.0.html
elasticsearch<7.14.0
# django-simple-history>3.0.0 adds indexing and causes a lot of migrations to be affected
+
+# opentelemetry requires version 6.x at the moment:
+# https://github.com/open-telemetry/opentelemetry-python/issues/3570
+# Normally this could be added as a constraint in edx-django-utils, where we're
+# adding the opentelemetry dependency. However, when we compile pip-tools.txt,
+# that uses version 7.x, and then there's no undoing that when compiling base.txt.
+# So we need to pin it globally, for now.
+# Ticket for unpinning: https://github.com/openedx/edx-lint/issues/407
+importlib-metadata<7
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index cfaecd0ca35a..393d016a73e7 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -181,8 +181,9 @@ defusedxml==0.7.1
# social-auth-core
deprecated==1.2.14
# via jwcrypto
-django==4.2.10
+django==4.2.11
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/kernel.in
# django-appconf
# django-celery-results
@@ -610,7 +611,9 @@ idna==3.4
# snowflake-connector-python
# yarl
importlib-metadata==6.8.0
- # via markdown
+ # via
+ # -c requirements/edx/../common_constraints.txt
+ # markdown
importlib-resources==6.1.0
# via
# jsonschema
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 87c17c1e74ae..7383f5747be7 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -343,8 +343,9 @@ distlib==0.3.7
# via
# -r requirements/edx/testing.txt
# virtualenv
-django==4.2.10
+django==4.2.11
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-appconf
@@ -1011,6 +1012,7 @@ import-linter==1.12.0
# via -r requirements/edx/testing.txt
importlib-metadata==6.8.0
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/../pip-tools.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index 9ec73dc14a36..026ffa4a9f7a 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -230,8 +230,9 @@ deprecated==1.2.14
# via
# -r requirements/edx/base.txt
# jwcrypto
-django==4.2.10
+django==4.2.11
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# django-appconf
# django-celery-results
@@ -711,6 +712,7 @@ imagesize==1.4.1
# via sphinx
importlib-metadata==6.8.0
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# markdown
# sphinx
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index a4b87c9178e4..202e33a40ee7 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -263,8 +263,9 @@ dill==0.3.7
# via pylint
distlib==0.3.7
# via virtualenv
-django==4.2.10
+django==4.2.11
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# django-appconf
# django-celery-results
@@ -767,6 +768,7 @@ import-linter==1.12.0
# via -r requirements/edx/testing.in
importlib-metadata==6.8.0
# via
+ # -c requirements/edx/../common_constraints.txt
# -r requirements/edx/base.txt
# markdown
# pytest-randomly
diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt
index ea283f4ad3ad..d2e5211cc4bd 100644
--- a/requirements/pip-tools.txt
+++ b/requirements/pip-tools.txt
@@ -11,7 +11,9 @@ click==8.1.6
# -c requirements/constraints.txt
# pip-tools
importlib-metadata==6.8.0
- # via build
+ # via
+ # -c requirements/common_constraints.txt
+ # build
packaging==23.2
# via build
pip-tools==7.3.0