diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py
index 2c5be351fc50..a91379797060 100644
--- a/cms/djangoapps/contentstore/tests/test_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_utils.py
@@ -974,3 +974,18 @@ def test_if_content_is_plain_text(self):
assert Notification.objects.all().count() == 1
notification = Notification.objects.first()
assert notification.content == "
content Sub content heading
"
+
+ def test_if_html_unescapes(self):
+ """
+ Tests if html unescapes when creating content of course update notification
+ """
+ user = UserFactory()
+ CourseEnrollment.enroll(user=user, course_key=self.course.id)
+ assert Notification.objects.all().count() == 0
+ content = "<p> </p>
"\
+ "<p>abcd</p>
"\
+ "<p> </p>
"
+ send_course_update_notification(self.course.id, content, self.user)
+ assert Notification.objects.all().count() == 1
+ notification = Notification.objects.first()
+ assert notification.content == "abcd
"
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 964f0c57e757..df1d1e27b405 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -3,6 +3,7 @@
"""
from __future__ import annotations
import configparser
+import html
import logging
import re
from collections import defaultdict
@@ -2258,6 +2259,7 @@ def clean_html_body(html_body):
"""
Get html body, remove tags and limit to 500 characters
"""
+ html_body = html.unescape(html_body).strip()
html_body = BeautifulSoup(Truncator(html_body).chars(500, html=True), 'html.parser')
text_content = html_body.get_text(separator=" ").strip()
text_content = text_content.replace('\n', '').replace('\r', '')
diff --git a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
index a818da81d10f..68771ceaec04 100644
--- a/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
+++ b/cms/djangoapps/contentstore/views/tests/test_clipboard_paste.py
@@ -408,7 +408,6 @@ def setUp(self):
self.store = modulestore()
self.library = library_api.create_library(
- library_type=library_api.COMPLEX,
org=Organization.objects.create(name="Test Org", short_name="CL-TEST"),
slug="lib",
title="Library",
diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py
index df8d8dc6251f..3b9ba5798a01 100644
--- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py
+++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py
@@ -558,6 +558,7 @@ def _create_block(request):
"locator": str(created_xblock.location),
"courseKey": str(created_xblock.location.course_key),
"static_file_notices": asdict(notices),
+ "upstreamRef": str(created_xblock.upstream),
})
category = request.json["category"]
diff --git a/docs/docs_settings.py b/docs/docs_settings.py
index f12848876e8a..f791b2faafb9 100644
--- a/docs/docs_settings.py
+++ b/docs/docs_settings.py
@@ -4,7 +4,7 @@
import all the Studio code.
"""
-
+from textwrap import dedent
import os
from openedx.core.lib.derived import derive_settings
@@ -27,18 +27,71 @@
FEATURES[key] = True
# Settings that will fail if we enable them, and we don't need them for docs anyway.
-FEATURES['RUN_AS_ANALYTICS_SERVER_ENABLED'] = False
-FEATURES['ENABLE_SOFTWARE_SECURE_FAKE'] = False
-FEATURES['ENABLE_MKTG_SITE'] = False
+FEATURES["RUN_AS_ANALYTICS_SERVER_ENABLED"] = False
+FEATURES["ENABLE_SOFTWARE_SECURE_FAKE"] = False
+FEATURES["ENABLE_MKTG_SITE"] = False
+
+INSTALLED_APPS.extend(
+ [
+ "cms.djangoapps.contentstore.apps.ContentstoreConfig",
+ "cms.djangoapps.course_creators",
+ "cms.djangoapps.xblock_config.apps.XBlockConfig",
+ "lms.djangoapps.lti_provider",
+ ]
+)
+
+# Swagger generation details
+openapi_security_info_basic = (
+ "Obtain with a `POST` request to `/user/v1/account/login_session/`. "
+ "If needed, copy the cookies from the response to your new call."
+)
+openapi_security_info_jwt = dedent(
+ """
+ Obtain by making a `POST` request to `/oauth2/v1/access_token`.
+
+ You will need to be logged in and have a client ID and secret already created.
-INSTALLED_APPS.extend([
- 'cms.djangoapps.contentstore.apps.ContentstoreConfig',
- 'cms.djangoapps.course_creators',
- 'cms.djangoapps.xblock_config.apps.XBlockConfig',
- 'lms.djangoapps.lti_provider',
-])
+ Your request should have the headers
+
+ ```
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ ```
+
+ Your request should have the data payload
+
+ ```
+ 'grant_type': 'client_credentials'
+ 'client_id': [your client ID]
+ 'client_secret': [your client secret]
+ 'token_type': 'jwt'
+ ```
+
+ Your JWT will be returned in the response as `access_token`. Prefix with `JWT ` in your header.
+ """
+)
+openapi_security_info_csrf = (
+ "Obtain by making a `GET` request to `/csrf/api/v1/token`. The token will be in the response cookie `csrftoken`."
+)
+SWAGGER_SETTINGS["SECURITY_DEFINITIONS"] = {
+ "Basic": {
+ "type": "basic",
+ "description": openapi_security_info_basic,
+ },
+ "jwt": {
+ "type": "apiKey",
+ "name": "Authorization",
+ "in": "header",
+ "description": openapi_security_info_jwt,
+ },
+ "csrf": {
+ "type": "apiKey",
+ "name": "X-CSRFToken",
+ "in": "header",
+ "description": openapi_security_info_csrf,
+ },
+}
-COMMON_TEST_DATA_ROOT = ''
+COMMON_TEST_DATA_ROOT = ""
derive_settings(__name__)
diff --git a/docs/lms-openapi.yaml b/docs/lms-openapi.yaml
index 5e9afcc6d370..8011f84b4573 100644
--- a/docs/lms-openapi.yaml
+++ b/docs/lms-openapi.yaml
@@ -13,8 +13,44 @@ produces:
securityDefinitions:
Basic:
type: basic
+ description: Obtain with a `POST` request to `/user/v1/account/login_session/`. If
+ needed, copy the cookies from the response to your new call.
+ jwt:
+ type: apiKey
+ name: Authorization
+ in: header
+ description: |2
+
+ Obtain by making a `POST` request to `/oauth2/v1/access_token`.
+
+ You will need to be logged in and have a client ID and secret already created.
+
+ Your request should have the headers
+
+ ```
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ ```
+
+ Your request should have the data payload
+
+ ```
+ 'grant_type': 'client_credentials'
+ 'client_id': [your client ID]
+ 'client_secret': [your client secret]
+ 'token_type': 'jwt'
+ ```
+
+ Your JWT will be returned in the response as `access_token`. Prefix with `JWT ` in your header.
+ csrf:
+ type: apiKey
+ name: X-CSRFToken
+ in: header
+ description: Obtain by making a `GET` request to `/csrf/api/v1/token`. The token
+ will be in the response cookie `csrftoken`.
security:
- Basic: []
+- csrf: []
+- jwt: []
paths:
/agreements/v1/integrity_signature/{course_id}:
get:
@@ -3975,6 +4011,7 @@ paths:
"profile_name": "Jon Doe"
"verification_attempt_id": (Optional)
"proctored_exam_attempt_id": (Optional)
+ "platform_verification_attempt_id": (Optional)
"status": (Optional)
}
parameters:
@@ -4130,6 +4167,7 @@ paths:
"profile_name": "Jon Doe"
"verification_attempt_id": (Optional)
"proctored_exam_attempt_id": (Optional)
+ "platform_verification_attempt_id": (Optional)
"status": (Optional)
}
parameters:
@@ -6788,6 +6826,59 @@ paths:
in: path
required: true
type: string
+ /mobile/{api_version}/notifications/create-token/:
+ post:
+ operationId: mobile_notifications_create-token_create
+ summary: |-
+ **Use Case**
+ This endpoint allows clients to register a device for push notifications.
+ description: |-
+ If the device is already registered, the existing registration will be updated.
+ If setting PUSH_NOTIFICATIONS_SETTINGS is not configured, the endpoint will return a 501 error.
+
+ **Example Request**
+ POST /api/mobile/{version}/notifications/create-token/
+ **POST Parameters**
+ The body of the POST request can include the following parameters.
+ * name (optional) - A name of the device.
+ * registration_id (required) - The device token of the device.
+ * device_id (optional) - ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)
+ * active (optional) - Whether the device is active, default is True.
+ If False, the device will not receive notifications.
+ * cloud_message_type (required) - You should choose FCM or GCM. Currently, only FCM is supported.
+ * application_id (optional) - Opaque application identity, should be filled in for multiple
+ key/certificate access. Should be equal settings.FCM_APP_NAME.
+ **Example Response**
+ ```json
+ {
+ "id": 1,
+ "name": "My Device",
+ "registration_id": "fj3j4",
+ "device_id": 1234,
+ "active": true,
+ "date_created": "2024-04-18T07:39:37.132787Z",
+ "cloud_message_type": "FCM",
+ "application_id": "my_app_id"
+ }
+ ```
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ $ref: '#/definitions/GCMDevice'
+ responses:
+ '201':
+ description: ''
+ schema:
+ $ref: '#/definitions/GCMDevice'
+ tags:
+ - mobile
+ parameters:
+ - name: api_version
+ in: path
+ required: true
+ type: string
/mobile/{api_version}/users/{username}:
get:
operationId: mobile_users_read
@@ -8849,22 +8940,6 @@ paths:
tags:
- user
parameters: []
- /user/v1/accounts/verifications/{attempt_id}/:
- get:
- operationId: user_v1_accounts_verifications_read
- description: Get IDV attempt details by attempt_id. Only accessible by global
- staff.
- parameters: []
- responses:
- '200':
- description: ''
- tags:
- - user
- parameters:
- - name: attempt_id
- in: path
- required: true
- type: string
/user/v1/accounts/{username}:
get:
operationId: user_v1_accounts_read
@@ -9423,22 +9498,57 @@ paths:
- user
post:
operationId: user_account_login_session_create
- summary: Log in a user.
- description: |-
- See `login_user` for details.
-
- Example Usage:
-
- POST /api/user/v1/login_session
- with POST params `email`, `password`.
-
- 200 {'success': true}
- parameters: []
+ summary: POST /user/{api_version}/account/login_session/
+ description: Returns 200 on success, and a detailed error message otherwise.
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ email:
+ type: string
+ password:
+ type: string
responses:
- '201':
+ '200':
+ description: ''
+ schema:
+ type: object
+ properties:
+ success:
+ type: boolean
+ value:
+ type: string
+ error_code:
+ type: string
+ '400':
+ description: ''
+ schema:
+ type: object
+ properties:
+ success:
+ type: boolean
+ value:
+ type: string
+ error_code:
+ type: string
+ '403':
description: ''
+ schema:
+ type: object
+ properties:
+ success:
+ type: boolean
+ value:
+ type: string
+ error_code:
+ type: string
tags:
- user
+ security:
+ - csrf: []
parameters:
- name: api_version
in: path
@@ -10047,6 +10157,7 @@ definitions:
required:
- celebrations
- course_access
+ - studio_access
- course_id
- is_enrolled
- is_self_paced
@@ -10084,6 +10195,9 @@ definitions:
additionalProperties:
type: string
x-nullable: true
+ studio_access:
+ title: Studio access
+ type: boolean
course_id:
title: Course id
type: string
@@ -11237,10 +11351,24 @@ definitions:
title: Verification attempt id
type: integer
x-nullable: true
+ verification_attempt_status:
+ title: Verification attempt status
+ type: string
+ minLength: 1
+ x-nullable: true
proctored_exam_attempt_id:
title: Proctored exam attempt id
type: integer
x-nullable: true
+ platform_verification_attempt_id:
+ title: Platform verification attempt id
+ type: integer
+ x-nullable: true
+ platform_verification_attempt_status:
+ title: Platform verification attempt status
+ type: string
+ minLength: 1
+ x-nullable: true
status:
title: Status
type: string
@@ -11277,10 +11405,24 @@ definitions:
title: Verification attempt id
type: integer
x-nullable: true
+ verification_attempt_status:
+ title: Verification attempt status
+ type: string
+ minLength: 1
+ x-nullable: true
proctored_exam_attempt_id:
title: Proctored exam attempt id
type: integer
x-nullable: true
+ platform_verification_attempt_id:
+ title: Platform verification attempt id
+ type: integer
+ x-nullable: true
+ platform_verification_attempt_status:
+ title: Platform verification attempt status
+ type: string
+ minLength: 1
+ x-nullable: true
status:
title: Status
type: string
@@ -11710,6 +11852,52 @@ definitions:
title: Enddatetime
type: string
format: date-time
+ GCMDevice:
+ required:
+ - registration_id
+ type: object
+ properties:
+ id:
+ title: ID
+ type: integer
+ name:
+ title: Name
+ type: string
+ maxLength: 255
+ x-nullable: true
+ registration_id:
+ title: Registration ID
+ type: string
+ minLength: 1
+ device_id:
+ title: Device id
+ description: 'ANDROID_ID / TelephonyManager.getDeviceId() (e.g: 0x01)'
+ type: integer
+ x-nullable: true
+ active:
+ title: Is active
+ description: Inactive devices will not be sent notifications
+ type: boolean
+ date_created:
+ title: Creation date
+ type: string
+ format: date-time
+ readOnly: true
+ x-nullable: true
+ cloud_message_type:
+ title: Cloud Message Type
+ description: You should choose FCM, GCM is deprecated
+ type: string
+ enum:
+ - FCM
+ - GCM
+ application_id:
+ title: Application ID
+ description: Opaque application identity, should be filled in for multiple
+ key/certificate access
+ type: string
+ maxLength: 64
+ x-nullable: true
mobile_api.User:
required:
- username
diff --git a/lms/djangoapps/bulk_email/tasks.py b/lms/djangoapps/bulk_email/tasks.py
index dc118949530c..197a7c0f136e 100644
--- a/lms/djangoapps/bulk_email/tasks.py
+++ b/lms/djangoapps/bulk_email/tasks.py
@@ -537,6 +537,11 @@ def _send_course_email(entry_id, email_id, to_list, global_email_context, subtas
email_context['course_id'] = str(course_email.course_id)
email_context['unsubscribe_link'] = get_unsubscribed_link(current_recipient['username'],
str(course_email.course_id))
+ email_context['unsubscribe_text'] = 'Unsubscribe from course updates for this course'
+ email_context['disclaimer'] = (
+ "You are receiving this email because you are enrolled in the "
+ f"{email_context['platform_name']} course {email_context['course_title']}"
+ )
if is_bulk_email_edx_ace_enabled():
message = ACEEmail(site, email_context)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 2f47e006c693..c7e38441e35b 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -50,6 +50,7 @@
from django.utils.translation import gettext_lazy as _
from enterprise.constants import (
ENTERPRISE_ADMIN_ROLE,
+ ENTERPRISE_LEARNER_ROLE,
ENTERPRISE_CATALOG_ADMIN_ROLE,
ENTERPRISE_DASHBOARD_ADMIN_ROLE,
ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE,
@@ -60,6 +61,7 @@
SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE,
PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE,
PROVISIONING_PENDING_ENTERPRISE_CUSTOMER_ADMIN_ROLE,
+ DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
)
from openedx.core.constants import COURSE_KEY_REGEX, COURSE_KEY_PATTERN, COURSE_ID_PATTERN
@@ -4722,11 +4724,15 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
ENTERPRISE_CUSTOMER_COOKIE_NAME = 'enterprise_customer_uuid'
BASE_COOKIE_DOMAIN = 'localhost'
SYSTEM_TO_FEATURE_ROLE_MAPPING = {
+ ENTERPRISE_LEARNER_ROLE: [
+ DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
+ ],
ENTERPRISE_ADMIN_ROLE: [
ENTERPRISE_DASHBOARD_ADMIN_ROLE,
ENTERPRISE_CATALOG_ADMIN_ROLE,
ENTERPRISE_ENROLLMENT_API_ADMIN_ROLE,
ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE,
+ DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
],
ENTERPRISE_OPERATOR_ROLE: [
ENTERPRISE_DASHBOARD_ADMIN_ROLE,
@@ -4735,6 +4741,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
ENTERPRISE_REPORTING_CONFIG_ADMIN_ROLE,
ENTERPRISE_FULFILLMENT_OPERATOR_ROLE,
ENTERPRISE_SSO_ORCHESTRATOR_OPERATOR_ROLE,
+ DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_ROLE,
],
SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE: [
PROVISIONING_ENTERPRISE_CUSTOMER_ADMIN_ROLE,
diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py
index c89d84490e96..990226f343cf 100644
--- a/openedx/core/djangoapps/content/search/tests/test_api.py
+++ b/openedx/core/djangoapps/content/search/tests/test_api.py
@@ -115,7 +115,6 @@ def setUp(self):
# Create a content library:
self.library = library_api.create_library(
- library_type=library_api.COMPLEX,
org=OrganizationFactory.create(short_name="org1"),
slug="lib",
title="Library",
diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py
index 9911795a2a85..7e2b1f1fbbd5 100644
--- a/openedx/core/djangoapps/content_libraries/api.py
+++ b/openedx/core/djangoapps/content_libraries/api.py
@@ -114,7 +114,7 @@
from xmodule.modulestore.django import modulestore
from . import permissions, tasks
-from .constants import ALL_RIGHTS_RESERVED, COMPLEX
+from .constants import ALL_RIGHTS_RESERVED
from .models import ContentLibrary, ContentLibraryPermission, ContentLibraryBlockImportTask
log = logging.getLogger(__name__)
@@ -176,7 +176,6 @@ class ContentLibraryMetadata:
description = attr.ib("")
num_blocks = attr.ib(0)
version = attr.ib(0)
- type = attr.ib(default=COMPLEX)
last_published = attr.ib(default=None, type=datetime)
last_draft_created = attr.ib(default=None, type=datetime)
last_draft_created_by = attr.ib(default=None, type=datetime)
@@ -306,15 +305,13 @@ class LibraryXBlockType:
# ============
-def get_libraries_for_user(user, org=None, library_type=None, text_search=None, order=None):
+def get_libraries_for_user(user, org=None, text_search=None, order=None):
"""
Return content libraries that the user has permission to view.
"""
filter_kwargs = {}
if org:
filter_kwargs['org__short_name'] = org
- if library_type:
- filter_kwargs['type'] = library_type
qs = ContentLibrary.objects.filter(**filter_kwargs) \
.select_related('learning_package', 'org') \
.order_by('org__short_name', 'slug')
@@ -361,7 +358,6 @@ def get_metadata(queryset, text_search=None):
ContentLibraryMetadata(
key=lib.library_key,
title=lib.learning_package.title if lib.learning_package else "",
- type=lib.type,
description="",
version=0,
allow_public_learning=lib.allow_public_learning,
@@ -446,7 +442,6 @@ def get_library(library_key):
return ContentLibraryMetadata(
key=library_key,
title=learning_package.title,
- type=ref.type,
description=ref.learning_package.description,
num_blocks=num_blocks,
version=version,
@@ -474,7 +469,6 @@ def create_library(
allow_public_learning=False,
allow_public_read=False,
library_license=ALL_RIGHTS_RESERVED,
- library_type=COMPLEX,
):
"""
Create a new content library.
@@ -491,8 +485,6 @@ def create_library(
allow_public_read: Allow anyone to view blocks (including source) in Studio?
- library_type: Deprecated parameter, not really used. Set to COMPLEX.
-
Returns a ContentLibraryMetadata instance.
"""
assert isinstance(org, Organization)
@@ -502,7 +494,6 @@ def create_library(
ref = ContentLibrary.objects.create(
org=org,
slug=slug,
- type=library_type,
allow_public_learning=allow_public_learning,
allow_public_read=allow_public_read,
license=library_license,
@@ -526,7 +517,6 @@ def create_library(
return ContentLibraryMetadata(
key=ref.library_key,
title=title,
- type=library_type,
description=description,
num_blocks=0,
version=0,
@@ -611,7 +601,6 @@ def update_library(
description=None,
allow_public_learning=None,
allow_public_read=None,
- library_type=None,
library_license=None,
):
"""
@@ -621,7 +610,7 @@ def update_library(
A value of None means "don't change".
"""
lib_obj_fields = [
- allow_public_learning, allow_public_read, library_type, library_license
+ allow_public_learning, allow_public_read, library_license
]
lib_obj_changed = any(field is not None for field in lib_obj_fields)
learning_pkg_changed = any(field is not None for field in [title, description])
@@ -640,10 +629,6 @@ def update_library(
content_lib.allow_public_learning = allow_public_learning
if allow_public_read is not None:
content_lib.allow_public_read = allow_public_read
- if library_type is not None:
- # TODO: Get rid of this field entirely, and remove library_type
- # from any functions that take it as an argument.
- content_lib.library_type = library_type
if library_license is not None:
content_lib.library_license = library_license
content_lib.save()
@@ -856,13 +841,6 @@ def validate_can_add_block_to_library(
"""
assert isinstance(library_key, LibraryLocatorV2)
content_library = ContentLibrary.objects.get_by_key(library_key) # type: ignore[attr-defined]
- if content_library.type != COMPLEX:
- if block_type != content_library.type:
- raise IncompatibleTypesError(
- _('Block type "{block_type}" is not compatible with library type "{library_type}".').format(
- block_type=block_type, library_type=content_library.type,
- )
- )
# If adding a component would take us over our max, return an error.
component_count = authoring_api.get_all_drafts(content_library.learning_package.id).count()
@@ -1288,10 +1266,7 @@ def get_allowed_block_types(library_key): # pylint: disable=unused-argument
# TODO: return support status and template options
# See cms/djangoapps/contentstore/views/component.py
block_types = sorted(name for name, class_ in XBlock.load_classes())
- lib = get_library(library_key)
- if lib.type != COMPLEX:
- # Problem and Video libraries only permit XBlocks of the same name.
- block_types = (name for name in block_types if name == lib.type)
+
info = []
for block_type in block_types:
# TODO: unify the contentstore helper with the xblock.api version of
diff --git a/openedx/core/djangoapps/content_libraries/constants.py b/openedx/core/djangoapps/content_libraries/constants.py
index 9505d52d1cca..a01e0fbdda91 100644
--- a/openedx/core/djangoapps/content_libraries/constants.py
+++ b/openedx/core/djangoapps/content_libraries/constants.py
@@ -2,16 +2,6 @@
from django.utils.translation import gettext_lazy as _
-VIDEO = 'video'
-COMPLEX = 'complex'
-PROBLEM = 'problem'
-
-LIBRARY_TYPES = (
- (VIDEO, _('Video')),
- (COMPLEX, _('Complex')),
- (PROBLEM, _('Problem')),
-)
-
# These are all the licenses we support so far.
ALL_RIGHTS_RESERVED = ''
CC_4_BY = 'CC:4.0:BY'
diff --git a/openedx/core/djangoapps/content_libraries/migrations/0011_remove_contentlibrary_bundle_uuid_and_more.py b/openedx/core/djangoapps/content_libraries/migrations/0011_remove_contentlibrary_bundle_uuid_and_more.py
new file mode 100644
index 000000000000..0c634c648870
--- /dev/null
+++ b/openedx/core/djangoapps/content_libraries/migrations/0011_remove_contentlibrary_bundle_uuid_and_more.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.2.16 on 2024-10-24 20:21
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('content_libraries', '0010_contentlibrary_learning_package_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='contentlibrary',
+ name='bundle_uuid',
+ ),
+ migrations.RemoveField(
+ model_name='contentlibrary',
+ name='type',
+ ),
+ ]
diff --git a/openedx/core/djangoapps/content_libraries/models.py b/openedx/core/djangoapps/content_libraries/models.py
index c1dea39e0613..4a210223cc29 100644
--- a/openedx/core/djangoapps/content_libraries/models.py
+++ b/openedx/core/djangoapps/content_libraries/models.py
@@ -54,8 +54,7 @@
from opaque_keys.edx.django.models import UsageKeyField
from openedx.core.djangoapps.content_libraries.constants import (
- LIBRARY_TYPES, COMPLEX, LICENSE_OPTIONS,
- ALL_RIGHTS_RESERVED,
+ LICENSE_OPTIONS, ALL_RIGHTS_RESERVED,
)
from openedx_learning.api.authoring_models import LearningPackage
from organizations.models import Organization # lint-amnesty, pylint: disable=wrong-import-order
@@ -101,18 +100,6 @@ class ContentLibrary(models.Model):
org = models.ForeignKey(Organization, on_delete=models.PROTECT, null=False)
slug = models.SlugField(allow_unicode=True)
- # We no longer use the ``bundle_uuid`` and ``type`` fields, but we'll leave
- # them in the model until after the Redwood release, just in case someone
- # out there was using v2 libraries. We don't expect this, since it wasn't in
- # a usable state, but there's always a chance someone managed to do it and
- # is still using it. By keeping the schema backwards compatible, the thought
- # is that they would update to the latest version, notice their libraries
- # aren't working correctly, and still have the ability to recover their data
- # if the code was rolled back.
- # TODO: Remove these fields after the Redwood release is cut.
- bundle_uuid = models.UUIDField(unique=True, null=True, default=None)
- type = models.CharField(max_length=25, default=COMPLEX, choices=LIBRARY_TYPES)
-
license = models.CharField(max_length=25, default=ALL_RIGHTS_RESERVED, choices=LICENSE_OPTIONS)
learning_package = models.OneToOneField(
LearningPackage,
diff --git a/openedx/core/djangoapps/content_libraries/serializers.py b/openedx/core/djangoapps/content_libraries/serializers.py
index b19d27bed3bb..d639ed63afb0 100644
--- a/openedx/core/djangoapps/content_libraries/serializers.py
+++ b/openedx/core/djangoapps/content_libraries/serializers.py
@@ -11,8 +11,6 @@
from openedx_learning.api.authoring_models import Collection
from openedx.core.djangoapps.content_libraries.constants import (
- LIBRARY_TYPES,
- COMPLEX,
ALL_RIGHTS_RESERVED,
LICENSE_OPTIONS,
)
@@ -37,10 +35,8 @@ class ContentLibraryMetadataSerializer(serializers.Serializer):
# begins with 'lib:'. (The numeric ID of the ContentLibrary object in MySQL
# is not exposed via this API.)
id = serializers.CharField(source="key", read_only=True)
- type = serializers.ChoiceField(choices=LIBRARY_TYPES, default=COMPLEX)
org = serializers.SlugField(source="key.org")
slug = serializers.CharField(source="key.slug", validators=(validate_unicode_slug, ))
- bundle_uuid = serializers.UUIDField(format='hex_verbose', read_only=True)
title = serializers.CharField()
description = serializers.CharField(allow_blank=True)
num_blocks = serializers.IntegerField(read_only=True)
@@ -86,7 +82,6 @@ class ContentLibraryUpdateSerializer(serializers.Serializer):
description = serializers.CharField()
allow_public_learning = serializers.BooleanField()
allow_public_read = serializers.BooleanField()
- type = serializers.ChoiceField(choices=LIBRARY_TYPES)
license = serializers.ChoiceField(choices=LICENSE_OPTIONS)
@@ -118,7 +113,7 @@ class ContentLibraryPermissionSerializer(ContentLibraryPermissionLevelSerializer
group_name = serializers.CharField(source="group.name", allow_null=True, allow_blank=False, default=None)
-class BaseFilterSerializer(serializers.Serializer):
+class ContentLibraryFilterSerializer(serializers.Serializer):
"""
Base serializer for filtering listings on the content library APIs.
"""
@@ -127,13 +122,6 @@ class BaseFilterSerializer(serializers.Serializer):
order = serializers.CharField(default=None, required=False)
-class ContentLibraryFilterSerializer(BaseFilterSerializer):
- """
- Serializer for filtering library listings.
- """
- type = serializers.ChoiceField(choices=LIBRARY_TYPES, default=None, required=False)
-
-
class CollectionMetadataSerializer(serializers.Serializer):
"""
Serializer for CollectionMetadata
diff --git a/openedx/core/djangoapps/content_libraries/tests/base.py b/openedx/core/djangoapps/content_libraries/tests/base.py
index 33d5477da698..7f1664e6c7fc 100644
--- a/openedx/core/djangoapps/content_libraries/tests/base.py
+++ b/openedx/core/djangoapps/content_libraries/tests/base.py
@@ -9,7 +9,7 @@
from rest_framework.test import APITransactionTestCase, APIClient
from common.djangoapps.student.tests.factories import UserFactory
-from openedx.core.djangoapps.content_libraries.constants import COMPLEX, ALL_RIGHTS_RESERVED
+from openedx.core.djangoapps.content_libraries.constants import ALL_RIGHTS_RESERVED
from openedx.core.djangolib.testing.utils import skip_unless_cms
# Define the URLs here - don't use reverse() because we want to detect
@@ -124,7 +124,7 @@ def as_user(self, user):
self.client = old_client # pylint: disable=attribute-defined-outside-init
def _create_library(
- self, slug, title, description="", org=None, library_type=COMPLEX,
+ self, slug, title, description="", org=None,
license_type=ALL_RIGHTS_RESERVED, expect_response=200,
):
""" Create a library """
@@ -135,7 +135,6 @@ def _create_library(
"slug": slug,
"title": title,
"description": description,
- "type": library_type,
"license": license_type,
}, expect_response)
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
index 49175ef152fb..8b9d60d86be0 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_content_libraries.py
@@ -26,7 +26,7 @@
from rest_framework.test import APITestCase
from common.djangoapps.student.tests.factories import UserFactory
-from openedx.core.djangoapps.content_libraries.constants import CC_4_BY, COMPLEX, PROBLEM, VIDEO
+from openedx.core.djangoapps.content_libraries.constants import CC_4_BY
from openedx.core.djangoapps.content_libraries.tests.base import (
URL_BLOCK_GET_HANDLER_URL,
URL_BLOCK_METADATA_URL,
@@ -102,7 +102,6 @@ def test_library_crud(self):
"title": "A Tést Lꜟطrary",
"description": "Just Téstꜟng",
"version": 0,
- "type": COMPLEX,
"license": CC_4_BY,
"has_unpublished_changes": False,
"has_unpublished_deletes": False,
@@ -201,13 +200,13 @@ def test_library_filters(self):
Test the filters in the list libraries API
"""
self._create_library(
- slug="test-lib-filter-1", title="Fob", description="Bar", library_type=VIDEO,
+ slug="test-lib-filter-1", title="Fob", description="Bar",
)
self._create_library(
slug="test-lib-filter-2", title="Library-Title-2", description="Bar-2",
)
self._create_library(
- slug="l3", title="Library-Title-3", description="Description", library_type=VIDEO,
+ slug="l3", title="Library-Title-3", description="Description",
)
Organization.objects.get_or_create(
@@ -217,7 +216,6 @@ def test_library_filters(self):
self._create_library(
slug="l4", title="Library-Title-4",
description="Library-Description", org='org-test',
- library_type=VIDEO,
)
self._create_library(
slug="l5", title="Library-Title-5", description="Library-Description",
@@ -227,14 +225,11 @@ def test_library_filters(self):
assert len(self._list_libraries()) == 5
assert len(self._list_libraries({'org': 'org-test'})) == 2
assert len(self._list_libraries({'text_search': 'test-lib-filter'})) == 2
- assert len(self._list_libraries({'text_search': 'test-lib-filter', 'type': VIDEO})) == 1
assert len(self._list_libraries({'text_search': 'library-title'})) == 4
- assert len(self._list_libraries({'text_search': 'library-title', 'type': VIDEO})) == 2
assert len(self._list_libraries({'text_search': 'bar'})) == 2
assert len(self._list_libraries({'text_search': 'org-test'})) == 2
assert len(self._list_libraries({'org': 'org-test',
'text_search': 'library-title-4'})) == 1
- assert len(self._list_libraries({'type': VIDEO})) == 3
self.assertOrderEqual(
self._list_libraries({'order': 'title'}),
@@ -528,27 +523,6 @@ def test_library_blocks_filters(self):
assert len(self._get_library_blocks(lib['id'], {'block_type': 'problem'})['results']) == 3
assert len(self._get_library_blocks(lib['id'], {'block_type': 'squirrel'})['results']) == 0
- @ddt.data(
- ('video-problem', VIDEO, 'problem', 400),
- ('video-video', VIDEO, 'video', 200),
- ('problem-problem', PROBLEM, 'problem', 200),
- ('problem-video', PROBLEM, 'video', 400),
- ('complex-video', COMPLEX, 'video', 200),
- ('complex-problem', COMPLEX, 'problem', 200),
- )
- @ddt.unpack
- def test_library_blocks_type_constrained(self, slug, library_type, block_type, expect_response):
- """
- Test that type-constrained libraries enforce their constraint when adding an XBlock.
- """
- lib = self._create_library(
- slug=slug, title="A Test Library", description="Testing XBlocks", library_type=library_type,
- )
- lib_id = lib["id"]
-
- # Add a 'problem' XBlock to the library:
- self._add_block_to_library(lib_id, block_type, 'test-block', expect_response=expect_response)
-
def test_library_not_found(self):
"""Test that requests fail with 404 when the library does not exist"""
valid_not_found_key = 'lb:valid:key:video:1'
@@ -787,24 +761,6 @@ def test_library_blocks_limit(self):
# Second block should throw error
self._add_block_to_library(lib_id, "problem", "problem1", expect_response=400)
- @ddt.data(
- ('complex-types', COMPLEX, False),
- ('video-types', VIDEO, True),
- ('problem-types', PROBLEM, True),
- )
- @ddt.unpack
- def test_block_types(self, slug, library_type, constrained):
- """
- Test that the permitted block types listing for a library change based on type.
- """
- lib = self._create_library(slug=slug, title='Test Block Types', library_type=library_type)
- types = self._get_library_block_types(lib['id'])
- if constrained:
- assert len(types) == 1
- assert types[0]['block_type'] == library_type
- else:
- assert len(types) > 1
-
def test_content_library_create_event(self):
"""
Check that CONTENT_LIBRARY_CREATED event is sent when a content library is created.
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_models.py b/openedx/core/djangoapps/content_libraries/tests/test_models.py
index 81a5a8fa32f3..eded47a305d2 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_models.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_models.py
@@ -15,7 +15,6 @@
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
from ..models import ALL_RIGHTS_RESERVED
-from ..models import COMPLEX
from ..models import ContentLibrary
from ..models import LtiGradedResource
from ..models import LtiProfile
@@ -35,7 +34,6 @@ def _create_library(self, **kwds):
return ContentLibrary.objects.create(
org=org,
slug='foobar',
- type=COMPLEX,
allow_public_learning=False,
allow_public_read=False,
license=ALL_RIGHTS_RESERVED,
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py
index f79808a7ec9a..3b505d311162 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py
@@ -21,7 +21,7 @@
URL_BLOCK_FIELDS_URL,
)
from openedx.core.djangoapps.content_libraries.tests.user_state_block import UserStateTestBlock
-from openedx.core.djangoapps.content_libraries.constants import COMPLEX, ALL_RIGHTS_RESERVED
+from openedx.core.djangoapps.content_libraries.constants import ALL_RIGHTS_RESERVED
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
from openedx.core.djangoapps.xblock import api as xblock_api
from openedx.core.djangolib.testing.utils import skip_unless_lms, skip_unless_cms
@@ -50,7 +50,6 @@ def setUp(self):
_, slug = self.id().rsplit('.', 1)
with transaction.atomic():
self.library = library_api.create_library(
- library_type=COMPLEX,
org=self.organization,
slug=slugify(slug),
title=(f"{slug} Test Lib"),
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_views_lti.py b/openedx/core/djangoapps/content_libraries/tests/test_views_lti.py
index a25d02761dde..b1ee61ca50f9 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_views_lti.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_views_lti.py
@@ -5,8 +5,6 @@
from django.conf import settings
from django.test import TestCase, override_settings
-from openedx.core.djangoapps.content_libraries.constants import PROBLEM
-
from .base import (
ContentLibrariesRestApiTest,
URL_LIB_LTI_JWKS,
@@ -60,10 +58,10 @@ def test_lti_url_generation(self):
"""
library = self._create_library(
- slug="libgg", title="A Test Library", description="Testing library", library_type=PROBLEM,
+ slug="libgg", title="A Test Library", description="Testing library",
)
- block = self._add_block_to_library(library['id'], PROBLEM, PROBLEM)
+ block = self._add_block_to_library(library['id'], 'problem', 'problem')
usage_key = str(block['id'])
url = f'/api/libraries/v2/blocks/{usage_key}/lti/'
diff --git a/openedx/core/djangoapps/content_libraries/views.py b/openedx/core/djangoapps/content_libraries/views.py
index 9357d54ca7ce..6325d9c4aeb4 100644
--- a/openedx/core/djangoapps/content_libraries/views.py
+++ b/openedx/core/djangoapps/content_libraries/views.py
@@ -227,14 +227,12 @@ def get(self, request):
serializer = ContentLibraryFilterSerializer(data=request.query_params)
serializer.is_valid(raise_exception=True)
org = serializer.validated_data['org']
- library_type = serializer.validated_data['type']
text_search = serializer.validated_data['text_search']
order = serializer.validated_data['order']
queryset = api.get_libraries_for_user(
request.user,
org=org,
- library_type=library_type,
text_search=text_search,
order=order,
)
@@ -259,7 +257,6 @@ def post(self, request):
data = dict(serializer.validated_data)
# Converting this over because using the reserved names 'type' and 'license' would shadow the built-in
# definitions elsewhere.
- data['library_type'] = data.pop('type')
data['library_license'] = data.pop('license')
key_data = data.pop("key")
# Move "slug" out of the "key.slug" pseudo-field that the serializer added:
@@ -313,8 +310,6 @@ def patch(self, request, lib_key_str):
serializer.is_valid(raise_exception=True)
data = dict(serializer.validated_data)
# Prevent ourselves from shadowing global names.
- if 'type' in data:
- data['library_type'] = data.pop('type')
if 'license' in data:
data['library_license'] = data.pop('license')
try:
diff --git a/openedx/core/djangoapps/notifications/email/tests/test_utils.py b/openedx/core/djangoapps/notifications/email/tests/test_utils.py
index 6e79c497a4fe..1f3da983a020 100644
--- a/openedx/core/djangoapps/notifications/email/tests/test_utils.py
+++ b/openedx/core/djangoapps/notifications/email/tests/test_utils.py
@@ -16,7 +16,6 @@
COURSE_NOTIFICATION_TYPES,
)
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS
-from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification
from openedx.core.djangoapps.notifications.email.utils import (
add_additional_attributes_to_notifications,
@@ -320,9 +319,7 @@ def test_value_with_channel_param(self, param_channel, new_value):
if channel == param_channel:
assert type_prefs[channel] == new_value
if channel == 'email':
- cadence_value = EmailCadence.NEVER
- if new_value:
- cadence_value = self.get_default_cadence_value(app_name, noti_type)
+ cadence_value = self.get_default_cadence_value(app_name, noti_type)
assert type_prefs['email_cadence'] == cadence_value
else:
default_app_json = self.default_json[app_name]
@@ -381,9 +378,7 @@ def test_value_with_app_name_param(self, param_app_name, new_value):
if app_name == param_app_name:
assert type_prefs[channel] == new_value
if channel == 'email':
- cadence_value = EmailCadence.NEVER
- if new_value:
- cadence_value = self.get_default_cadence_value(app_name, noti_type)
+ cadence_value = self.get_default_cadence_value(app_name, noti_type)
assert type_prefs['email_cadence'] == cadence_value
else:
default_app_json = self.default_json[app_name]
@@ -415,9 +410,7 @@ def test_value_with_notification_type_param(self, param_notification_type, new_v
if noti_type == param_notification_type:
assert type_prefs[channel] == new_value
if channel == 'email':
- cadence_value = EmailCadence.NEVER
- if new_value:
- cadence_value = self.get_default_cadence_value(app_name, noti_type)
+ cadence_value = self.get_default_cadence_value(app_name, noti_type)
assert type_prefs['email_cadence'] == cadence_value
else:
default_app_json = self.default_json[app_name]
diff --git a/openedx/core/djangoapps/notifications/email/utils.py b/openedx/core/djangoapps/notifications/email/utils.py
index 582e867d629d..d855494012ea 100644
--- a/openedx/core/djangoapps/notifications/email/utils.py
+++ b/openedx/core/djangoapps/notifications/email/utils.py
@@ -406,9 +406,7 @@ def get_updated_preference(pref):
continue
if is_editable(app_name, noti_type, channel):
type_prefs[channel] = pref_value
- if channel == 'email':
- cadence_value = get_default_cadence_value(app_name, noti_type)\
- if pref_value else EmailCadence.NEVER
- type_prefs['email_cadence'] = cadence_value
+ if channel == 'email' and pref_value and type_prefs.get('email_cadence') == EmailCadence.NEVER:
+ type_prefs['email_cadence'] = get_default_cadence_value(app_name, noti_type)
preference.save()
notification_preference_unsubscribe_event(user)
diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py
index b7bd0414a27f..70e6fbc5739c 100644
--- a/openedx/core/djangoapps/notifications/tests/test_views.py
+++ b/openedx/core/djangoapps/notifications/tests/test_views.py
@@ -27,7 +27,6 @@
FORUM_ROLE_MODERATOR
)
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
-from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.models import (
CourseNotificationPreference,
Notification,
@@ -936,7 +935,6 @@ def test_if_preference_is_updated(self, request_type):
for app_name, app_prefs in config.items():
for type_prefs in app_prefs['notification_types'].values():
assert type_prefs['email'] is False
- assert type_prefs['email_cadence'] == EmailCadence.NEVER
def test_if_config_version_is_updated(self):
"""
diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py
index 2ad3c0ff18a0..6c7390406be1 100644
--- a/openedx/core/djangoapps/user_authn/views/login.py
+++ b/openedx/core/djangoapps/user_authn/views/login.py
@@ -23,11 +23,14 @@
from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.http import require_http_methods
from django_ratelimit.decorators import ratelimit
+from drf_yasg import openapi
+from drf_yasg.utils import swagger_auto_schema
from edx_django_utils.monitoring import set_custom_attribute
from eventtracking import tracker
from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
from openedx_filters.learning.filters import StudentLoginRequested
+from rest_framework import status
from rest_framework.views import APIView
from common.djangoapps import third_party_auth
@@ -49,7 +52,7 @@
from openedx.core.djangoapps.user_authn.tasks import check_pwned_password_and_send_track_event
from openedx.core.djangoapps.user_authn.toggles import (
is_require_third_party_auth_enabled,
- should_redirect_to_authn_microfrontend
+ should_redirect_to_authn_microfrontend,
)
from openedx.core.djangoapps.user_authn.views.login_form import get_login_session_form
from openedx.core.djangoapps.user_authn.views.password_reset import send_password_reset_email_for_user
@@ -62,7 +65,7 @@
log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
USER_MODEL = get_user_model()
-PASSWORD_RESET_INITIATED = 'edx.user.passwordreset.initiated'
+PASSWORD_RESET_INITIATED = "edx.user.passwordreset.initiated"
def _do_third_party_auth(request):
@@ -70,9 +73,9 @@ def _do_third_party_auth(request):
User is already authenticated via 3rd party, now try to find and return their associated Django user.
"""
running_pipeline = pipeline.get(request)
- username = running_pipeline['kwargs'].get('username')
- backend_name = running_pipeline['backend']
- third_party_uid = running_pipeline['kwargs']['uid']
+ username = running_pipeline["kwargs"].get("username")
+ backend_name = running_pipeline["backend"]
+ third_party_uid = running_pipeline["kwargs"]["uid"]
requested_provider = provider.Registry.get_from_pipeline(running_pipeline)
platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME)
@@ -81,26 +84,25 @@ def _do_third_party_auth(request):
except USER_MODEL.DoesNotExist:
AUDIT_LOG.info(
"Login failed - user with username {username} has no social auth "
- "with backend_name {backend_name}".format(
- username=username, backend_name=backend_name)
+ "with backend_name {backend_name}".format(username=username, backend_name=backend_name)
)
- message = Text(_(
- "You've successfully signed in to your {provider_name} account, "
- "but this account isn't linked with your {platform_name} account yet. {blank_lines}"
- "Use your {platform_name} username and password to sign in to {platform_name} below, "
- "and then link your {platform_name} account with {provider_name} from your dashboard. {blank_lines}"
- "If you don't have an account on {platform_name} yet, "
- "click {register_label_strong} at the top of the page."
- )).format(
- blank_lines=HTML('
'),
+ message = Text(
+ _(
+ "You've successfully signed in to your {provider_name} account, "
+ "but this account isn't linked with your {platform_name} account yet. {blank_lines}"
+ "Use your {platform_name} username and password to sign in to {platform_name} below, "
+ "and then link your {platform_name} account with {provider_name} from your dashboard. {blank_lines}"
+ "If you don't have an account on {platform_name} yet, "
+ "click {register_label_strong} at the top of the page."
+ )
+ ).format(
+ blank_lines=HTML("
"),
platform_name=platform_name,
provider_name=requested_provider.name,
- register_label_strong=HTML('{register_text}').format(
- register_text=_('Register')
- )
+ register_label_strong=HTML("{register_text}").format(register_text=_("Register")),
)
- raise AuthFailedError(message, error_code='third-party-auth-with-no-linked-account') # lint-amnesty, pylint: disable=raise-missing-from
+ raise AuthFailedError(message, error_code="third-party-auth-with-no-linked-account") # lint-amnesty, pylint: disable=raise-missing-from
def _get_user_by_email(email):
@@ -128,14 +130,14 @@ def _get_user_by_email_or_username(request, api_version):
Finds a user object in the database based on the given request, ignores all fields except for email and username.
"""
is_api_v2 = api_version != API_V1
- login_fields = ['email', 'password']
+ login_fields = ["email", "password"]
if is_api_v2:
- login_fields = ['email_or_username', 'password']
+ login_fields = ["email_or_username", "password"]
if any(f not in request.POST.keys() for f in login_fields):
- raise AuthFailedError(_('There was an error receiving your login information. Please email us.'))
+ raise AuthFailedError(_("There was an error receiving your login information. Please email us."))
- email_or_username = request.POST.get('email', None) or request.POST.get('email_or_username', None)
+ email_or_username = request.POST.get("email", None) or request.POST.get("email_or_username", None)
user = _get_user_by_email(email_or_username)
if not user and is_api_v2:
@@ -143,7 +145,7 @@ def _get_user_by_email_or_username(request, api_version):
user = _get_user_by_username(email_or_username)
if not user:
- digest = hashlib.shake_128(email_or_username.encode('utf-8')).hexdigest(16)
+ digest = hashlib.shake_128(email_or_username.encode("utf-8")).hexdigest(16)
AUDIT_LOG.warning(f"Login failed - Unknown user email or username {digest}")
return user
@@ -165,27 +167,30 @@ def _generate_locked_out_error_message():
"""
locked_out_period_in_sec = settings.MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS
- error_message = Text(_('To protect your account, it’s been temporarily '
- 'locked. Try again in {locked_out_period} minutes.'
- '{li_start}To be on the safe side, you can reset your '
- 'password {link_start}here{link_end} before you try again.')).format(
- link_start=HTML(''),
- link_end=HTML(''),
- li_start=HTML(''),
- li_end=HTML(''),
- locked_out_period=int(locked_out_period_in_sec / 60))
+ error_message = Text(
+ _(
+ "To protect your account, it’s been temporarily "
+ "locked. Try again in {locked_out_period} minutes."
+ "{li_start}To be on the safe side, you can reset your "
+ "password {link_start}here{link_end} before you try again."
+ )
+ ).format(
+ link_start=HTML(''),
+ link_end=HTML(""),
+ li_start=HTML(""),
+ li_end=HTML(""),
+ locked_out_period=int(locked_out_period_in_sec / 60),
+ )
raise AuthFailedError(
error_message,
- error_code='account-locked-out',
- context={
- 'locked_out_period': int(locked_out_period_in_sec / 60)
- }
+ error_code="account-locked-out",
+ context={"locked_out_period": int(locked_out_period_in_sec / 60)},
)
def _enforce_password_policy_compliance(request, user): # lint-amnesty, pylint: disable=missing-function-docstring
try:
- password_policy_compliance.enforce_compliance_on_login(user, request.POST.get('password'))
+ password_policy_compliance.enforce_compliance_on_login(user, request.POST.get("password"))
except password_policy_compliance.NonCompliantPasswordWarning as e:
# Allow login, but warn the user that they will be required to reset their password soon.
PageLevelMessages.register_warning_message(request, HTML(str(e)))
@@ -201,7 +206,7 @@ def _enforce_password_policy_compliance(request, user): # lint-amnesty, pylint:
{
"user_id": user.id,
"source": "Policy Compliance",
- }
+ },
)
send_password_reset_email_for_user(user, request)
@@ -214,19 +219,17 @@ def _log_and_raise_inactive_user_auth_error(unauthenticated_user):
Depending on Django version we can get here a couple of ways, but this takes care of logging an auth attempt
by an inactive user, re-sending the activation email, and raising an error with the correct message.
"""
- AUDIT_LOG.warning(
- f"Login failed - Account not active for user.id: {unauthenticated_user.id}, resending activation"
- )
+ AUDIT_LOG.warning(f"Login failed - Account not active for user.id: {unauthenticated_user.id}, resending activation")
profile = UserProfile.objects.get(user=unauthenticated_user)
compose_and_send_activation_email(unauthenticated_user, profile)
raise AuthFailedError(
- error_code='inactive-user',
+ error_code="inactive-user",
context={
- 'platformName': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
- 'supportLink': configuration_helpers.get_value('SUPPORT_SITE_LINK', settings.SUPPORT_SITE_LINK)
- }
+ "platformName": configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME),
+ "supportLink": configuration_helpers.get_value("SUPPORT_SITE_LINK", settings.SUPPORT_SITE_LINK),
+ },
)
@@ -234,9 +237,11 @@ def _authenticate_first_party(request, unauthenticated_user, third_party_auth_re
"""
Use Django authentication on the given request, using rate limiting if configured
"""
- should_be_rate_limited = getattr(request, 'limited', False)
+ should_be_rate_limited = getattr(request, "limited", False)
if should_be_rate_limited:
- raise AuthFailedError(_('Too many failed login attempts. Try again later.')) # lint-amnesty, pylint: disable=raise-missing-from
+ raise AuthFailedError(
+ _("Too many failed login attempts. Try again later.")
+ ) # lint-amnesty, pylint: disable=raise-missing-from
# If the user doesn't exist, we want to set the username to an invalid username so that authentication is guaranteed
# to fail and we can take advantage of the ratelimited backend
@@ -248,12 +253,8 @@ def _authenticate_first_party(request, unauthenticated_user, third_party_auth_re
if not third_party_auth_requested:
_check_user_auth_flow(request.site, unauthenticated_user)
- password = normalize_password(request.POST['password'])
- return authenticate(
- username=username,
- password=password,
- request=request
- )
+ password = normalize_password(request.POST["password"])
+ return authenticate(username=username, password=password, request=request)
def _handle_failed_authentication(user, authenticated_user):
@@ -279,34 +280,37 @@ def _handle_failed_authentication(user, authenticated_user):
if not LoginFailures.is_user_locked_out(user):
max_failures_allowed = settings.MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED
remaining_attempts = max_failures_allowed - failure_count
- error_message = Text(_('Email or password is incorrect.'
- '{li_start}You have {remaining_attempts} more sign-in '
- 'attempts before your account is temporarily locked.{li_end}'
- '{li_start}If you\'ve forgotten your password, click '
- '{link_start}here{link_end} to reset.{li_end}')).format(
- link_start=HTML(
- ''
- ),
- link_end=HTML(''),
- li_start=HTML(''),
- li_end=HTML(''),
- remaining_attempts=remaining_attempts)
+ error_message = Text(
+ _(
+ "Email or password is incorrect."
+ "{li_start}You have {remaining_attempts} more sign-in "
+ "attempts before your account is temporarily locked.{li_end}"
+ "{li_start}If you've forgotten your password, click "
+ "{link_start}here{link_end} to reset.{li_end}"
+ )
+ ).format(
+ link_start=HTML(''),
+ link_end=HTML(""),
+ li_start=HTML(""),
+ li_end=HTML(""),
+ remaining_attempts=remaining_attempts,
+ )
raise AuthFailedError(
error_message,
- error_code='failed-login-attempt',
+ error_code="failed-login-attempt",
context={
- 'remaining_attempts': remaining_attempts,
- 'allowed_failure_attempts': max_failures_allowed,
- 'failure_count': failure_count,
- }
+ "remaining_attempts": remaining_attempts,
+ "allowed_failure_attempts": max_failures_allowed,
+ "failure_count": failure_count,
+ },
)
_generate_locked_out_error_message()
raise AuthFailedError(
- _('Email or password is incorrect.'),
- error_code='incorrect-email-or-password',
- context={'failure_count': failure_count},
+ _("Email or password is incorrect."),
+ error_code="incorrect-email-or-password",
+ context={"failure_count": failure_count},
)
@@ -352,25 +356,18 @@ def _track_user_login(user, request):
# .. pii_retirement: third_party
segment.identify(
user.id,
- {
- 'email': user.email,
- 'username': user.username
- },
+ {"email": user.email, "username": user.username},
{
# Disable MailChimp because we don't want to update the user's email
# and username in MailChimp on every page load. We only need to capture
# this data on registration/activation.
- 'MailChimp': False
- }
+ "MailChimp": False
+ },
)
segment.track(
user.id,
"edx.bi.user.account.authenticated",
- {
- 'category': "conversion",
- 'label': request.POST.get('course_id'),
- 'provider': None
- },
+ {"category": "conversion", "label": request.POST.get("course_id"), "provider": None},
)
@@ -380,20 +377,22 @@ def _create_message(site, root_url, allowed_domain):
to an allowed domain and not whitelisted then ask such users to login
through allowed domain SSO provider.
"""
- msg = Text(_(
- 'As {allowed_domain} user, You must login with your {allowed_domain} '
- '{link_start}{provider} account{link_end}.'
- )).format(
+ msg = Text(
+ _(
+ "As {allowed_domain} user, You must login with your {allowed_domain} "
+ "{link_start}{provider} account{link_end}."
+ )
+ ).format(
allowed_domain=allowed_domain,
link_start=HTML("").format(
- root_url=root_url if root_url else '',
- tpa_provider_link='{dashboard_url}?tpa_hint={tpa_hint}'.format(
- dashboard_url=reverse('dashboard'),
- tpa_hint=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_HINT'),
- )
+ root_url=root_url if root_url else "",
+ tpa_provider_link="{dashboard_url}?tpa_hint={tpa_hint}".format(
+ dashboard_url=reverse("dashboard"),
+ tpa_hint=site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_HINT"),
+ ),
),
- provider=site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_PROVIDER'),
- link_end=HTML("")
+ provider=site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_PROVIDER"),
+ link_end=HTML(""),
)
return msg
@@ -404,13 +403,13 @@ def _check_user_auth_flow(site, user):
then ask user to login through allowed domain SSO provider.
"""
if user and ENABLE_LOGIN_USING_THIRDPARTY_AUTH_ONLY.is_enabled():
- allowed_domain = site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_DOMAIN', '').lower()
- email_parts = user.email.split('@')
+ allowed_domain = site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_DOMAIN", "").lower()
+ email_parts = user.email.split("@")
if len(email_parts) != 2:
# User has a nonstandard email so we record their id.
# we don't record their e-mail in case there is sensitive info accidentally
# in there.
- set_custom_attribute('login_tpa_domain_shortcircuit_user_id', user.id)
+ set_custom_attribute("login_tpa_domain_shortcircuit_user_id", user.id)
log.warning("User %s has nonstandard e-mail. Shortcircuiting THIRD_PART_AUTH_ONLY_DOMAIN check.", user.id)
return
user_domain = email_parts[1].strip().lower()
@@ -422,19 +421,19 @@ def _check_user_auth_flow(site, user):
raise AuthFailedError(msg)
raise AuthFailedError(
- error_code='allowed-domain-login-error',
+ error_code="allowed-domain-login-error",
context={
- 'allowed_domain': allowed_domain,
- 'provider': site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_PROVIDER'),
- 'tpa_hint': site.configuration.get_value('THIRD_PARTY_AUTH_ONLY_HINT'),
- }
+ "allowed_domain": allowed_domain,
+ "provider": site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_PROVIDER"),
+ "tpa_hint": site.configuration.get_value("THIRD_PARTY_AUTH_ONLY_HINT"),
+ },
)
@login_required
-@require_http_methods(['GET'])
+@require_http_methods(["GET"])
def finish_auth(request):
- """ Following logistration (1st or 3rd party), handle any special query string params.
+ """Following logistration (1st or 3rd party), handle any special query string params.
See FinishAuthView.js for details on the query string params.
@@ -459,10 +458,13 @@ def finish_auth(request):
GET /account/finish_auth/?course_id=course-v1:blah&enrollment_action=enroll
"""
- return render_to_response('student_account/finish_auth.html', {
- 'disable_courseware_js': True,
- 'disable_footer': True,
- })
+ return render_to_response(
+ "student_account/finish_auth.html",
+ {
+ "disable_courseware_js": True,
+ "disable_footer": True,
+ },
+ )
def enterprise_selection_page(request, user, next_url):
@@ -478,14 +480,14 @@ def enterprise_selection_page(request, user, next_url):
response = get_enterprise_learner_data_from_api(user)
if response and len(response) > 1:
- redirect_url = reverse('enterprise_select_active') + '/?success_url=' + urllib.parse.quote(next_url)
+ redirect_url = reverse("enterprise_select_active") + "/?success_url=" + urllib.parse.quote(next_url)
# Check to see if next url has an enterprise in it. In this case if user is associated with
# that enterprise, activate that enterprise and bypass the selection page.
if re.match(ENTERPRISE_ENROLLMENT_URL_REGEX, urllib.parse.unquote(next_url)):
enterprise_in_url = re.search(UUID4_REGEX, next_url).group(0)
for enterprise in response:
- if enterprise_in_url == str(enterprise['enterprise_customer']['uuid']):
+ if enterprise_in_url == str(enterprise["enterprise_customer"]["uuid"]):
is_activated_successfully = activate_learner_enterprise(request, user, enterprise_in_url)
if is_activated_successfully:
redirect_url = next_url
@@ -495,20 +497,20 @@ def enterprise_selection_page(request, user, next_url):
@ensure_csrf_cookie
-@require_http_methods(['POST'])
+@require_http_methods(["POST"])
@ratelimit(
- key='openedx.core.djangoapps.util.ratelimit.request_post_email_or_username',
+ key="openedx.core.djangoapps.util.ratelimit.request_post_email_or_username",
rate=settings.LOGISTRATION_PER_EMAIL_RATELIMIT_RATE,
- method='POST',
+ method="POST",
block=False,
) # lint-amnesty, pylint: disable=too-many-statements
@ratelimit(
- key='openedx.core.djangoapps.util.ratelimit.real_ip',
+ key="openedx.core.djangoapps.util.ratelimit.real_ip",
rate=settings.LOGISTRATION_RATELIMIT_RATE,
- method='POST',
+ method="POST",
block=False,
) # lint-amnesty, pylint: disable=too-many-statements
-def login_user(request, api_version='v1'): # pylint: disable=too-many-statements
+def login_user(request, api_version="v1"): # pylint: disable=too-many-statements
"""
AJAX request to log in the user.
@@ -542,10 +544,10 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
_parse_analytics_param_for_course_id(request)
third_party_auth_requested = third_party_auth.is_enabled() and pipeline.running(request)
- first_party_auth_requested = any(bool(request.POST.get(p)) for p in ['email', 'email_or_username', 'password'])
+ first_party_auth_requested = any(bool(request.POST.get(p)) for p in ["email", "email_or_username", "password"])
is_user_third_party_authenticated = False
- set_custom_attribute('login_user_course_id', request.POST.get('course_id'))
+ set_custom_attribute("login_user_course_id", request.POST.get("course_id"))
if is_require_third_party_auth_enabled() and not third_party_auth_requested:
return HttpResponseForbidden(
@@ -564,12 +566,12 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
try:
user = _do_third_party_auth(request)
is_user_third_party_authenticated = True
- set_custom_attribute('login_user_tpa_success', True)
+ set_custom_attribute("login_user_tpa_success", True)
except AuthFailedError as e:
- set_custom_attribute('login_user_tpa_success', False)
- set_custom_attribute('login_user_tpa_failure_msg', e.value)
+ set_custom_attribute("login_user_tpa_success", False)
+ set_custom_attribute("login_user_tpa_failure_msg", e.value)
if e.error_code:
- set_custom_attribute('login_error_code', e.error_code)
+ set_custom_attribute("login_error_code", e.error_code)
# user successfully authenticated with a third party provider, but has no linked Open edX account
response_content = e.get_response()
@@ -585,7 +587,10 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
possibly_authenticated_user = StudentLoginRequested.run_filter(user=possibly_authenticated_user)
except StudentLoginRequested.PreventLogin as exc:
raise AuthFailedError(
- str(exc), redirect_url=exc.redirect_to, error_code=exc.error_code, context=exc.context,
+ str(exc),
+ redirect_url=exc.redirect_to,
+ error_code=exc.error_code,
+ context=exc.context,
) from exc
if not is_user_third_party_authenticated:
@@ -599,82 +604,82 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
):
_handle_failed_authentication(user, possibly_authenticated_user)
- pwned_properties = check_pwned_password_and_send_track_event(
- user_id=user.id,
- password=request.POST.get('password'),
- internal_user=user.is_staff,
- request_page='login'
- ) if not is_user_third_party_authenticated else {}
+ pwned_properties = (
+ check_pwned_password_and_send_track_event(
+ user_id=user.id,
+ password=request.POST.get("password"),
+ internal_user=user.is_staff,
+ request_page="login",
+ )
+ if not is_user_third_party_authenticated
+ else {}
+ )
# Set default for third party login
- password_frequency = pwned_properties.get('frequency', -1)
+ password_frequency = pwned_properties.get("frequency", -1)
if (
- settings.ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY and
- password_frequency >= settings.HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD
+ settings.ENABLE_AUTHN_LOGIN_BLOCK_HIBP_POLICY
+ and password_frequency >= settings.HIBP_LOGIN_BLOCK_PASSWORD_FREQUENCY_THRESHOLD
):
- raise VulnerablePasswordError(
- accounts.AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG,
- 'require-password-change'
- )
+ raise VulnerablePasswordError(accounts.AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG, "require-password-change")
_handle_successful_authentication_and_login(possibly_authenticated_user, request)
# The AJAX method calling should know the default destination upon success
- redirect_url, finish_auth_url = None, ''
+ redirect_url, finish_auth_url = None, ""
if third_party_auth_requested:
running_pipeline = pipeline.get(request)
- finish_auth_url = pipeline.get_complete_url(backend_name=running_pipeline['backend'])
+ finish_auth_url = pipeline.get_complete_url(backend_name=running_pipeline["backend"])
if is_user_third_party_authenticated:
redirect_url = finish_auth_url
elif should_redirect_to_authn_microfrontend():
next_url, root_url = get_next_url_for_login_page(request, include_host=True)
redirect_url = get_redirect_url_with_host(
- root_url,
- enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)
+ root_url, enterprise_selection_page(request, possibly_authenticated_user, finish_auth_url or next_url)
)
if (
- settings.ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY and
- 0 <= password_frequency <= settings.HIBP_LOGIN_NUDGE_PASSWORD_FREQUENCY_THRESHOLD
+ settings.ENABLE_AUTHN_LOGIN_NUDGE_HIBP_POLICY
+ and 0 <= password_frequency <= settings.HIBP_LOGIN_NUDGE_PASSWORD_FREQUENCY_THRESHOLD
):
raise VulnerablePasswordError(
- accounts.AUTHN_LOGIN_NUDGE_HIBP_POLICY_MSG,
- 'nudge-password-change',
- redirect_url
+ accounts.AUTHN_LOGIN_NUDGE_HIBP_POLICY_MSG, "nudge-password-change", redirect_url
)
- response = JsonResponse({
- 'success': True,
- 'redirect_url': redirect_url,
- })
+ response = JsonResponse(
+ {
+ "success": True,
+ "redirect_url": redirect_url,
+ }
+ )
# Ensure that the external marketing site can
# detect that the user is logged in.
response = set_logged_in_cookies(request, response, possibly_authenticated_user)
- set_custom_attribute('login_user_auth_failed_error', False)
- set_custom_attribute('login_user_response_status', response.status_code)
- set_custom_attribute('login_user_redirect_url', redirect_url)
+ set_custom_attribute("login_user_auth_failed_error", False)
+ set_custom_attribute("login_user_response_status", response.status_code)
+ set_custom_attribute("login_user_redirect_url", redirect_url)
mark_user_change_as_expected(user.id)
return response
except AuthFailedError as error:
response_content = error.get_response()
log.exception(response_content)
- error_code = response_content.get('error_code')
+ error_code = response_content.get("error_code")
if error_code:
- set_custom_attribute('login_error_code', error_code)
- email_or_username_key = 'email' if api_version == API_V1 else 'email_or_username'
+ set_custom_attribute("login_error_code", error_code)
+ email_or_username_key = "email" if api_version == API_V1 else "email_or_username"
email_or_username = request.POST.get(email_or_username_key, None)
email_or_username = possibly_authenticated_user.email if possibly_authenticated_user else email_or_username
- response_content['email'] = email_or_username
+ response_content["email"] = email_or_username
except VulnerablePasswordError as error:
response_content = error.get_response()
log.exception(response_content)
response = JsonResponse(response_content, status=400)
- set_custom_attribute('login_user_auth_failed_error', True)
- set_custom_attribute('login_user_response_status', response.status_code)
+ set_custom_attribute("login_user_auth_failed_error", True)
+ set_custom_attribute("login_user_response_status", response.status_code)
return response
@@ -683,10 +688,10 @@ def login_user(request, api_version='v1'): # pylint: disable=too-many-statement
# to get a CSRF token before we need to refresh adds too much
# complexity.
@csrf_exempt
-@require_http_methods(['POST'])
+@require_http_methods(["POST"])
def login_refresh(request): # lint-amnesty, pylint: disable=missing-function-docstring
if not request.user.is_authenticated or request.user.is_anonymous:
- return JsonResponse('Unauthorized', status=401)
+ return JsonResponse("Unauthorized", status=401)
try:
return get_response_with_refreshed_jwt_cookies(request, request.user)
@@ -700,33 +705,57 @@ def redirect_to_lms_login(request):
This view redirect the admin/login url to the site's login page if
waffle switch is on otherwise returns the admin site's login view.
"""
- return redirect('/login?next=/admin')
+ return redirect("/login?next=/admin")
+
+
+login_user_schema = openapi.Schema(
+ type=openapi.TYPE_OBJECT,
+ properties={
+ "email": openapi.Schema(type=openapi.TYPE_STRING),
+ "password": openapi.Schema(type=openapi.TYPE_STRING),
+ },
+)
+
+login_user_return_schema = openapi.Schema(
+ type=openapi.TYPE_OBJECT,
+ properties={
+ "success": openapi.Schema(type=openapi.TYPE_BOOLEAN),
+ "value": openapi.Schema(type=openapi.TYPE_STRING),
+ "error_code": openapi.Schema(type=openapi.TYPE_STRING),
+ },
+)
class LoginSessionView(APIView):
- """HTTP end-points for logging in users. """
+ """HTTP end-points for logging in users."""
# This end-point is available to anonymous users,
# so do not require authentication.
authentication_classes = []
+ login_user_responses = {
+ status.HTTP_200_OK: login_user_return_schema,
+ status.HTTP_400_BAD_REQUEST: login_user_return_schema,
+ status.HTTP_403_FORBIDDEN: login_user_return_schema,
+ }
+
@method_decorator(ensure_csrf_cookie)
def get(self, request, *args, **kwargs):
return HttpResponse(get_login_session_form(request).to_json(), content_type="application/json") # lint-amnesty, pylint: disable=http-response-with-content-type-json
+ @swagger_auto_schema(
+ request_body=login_user_schema,
+ responses=login_user_responses,
+ security=[
+ {"csrf": []},
+ ],
+ )
@method_decorator(csrf_protect)
def post(self, request, api_version):
- """Log in a user.
-
- See `login_user` for details.
-
- Example Usage:
-
- POST /api/user/v1/login_session
- with POST params `email`, `password`.
-
- 200 {'success': true}
+ """
+ POST /user/{api_version}/account/login_session/
+ Returns 200 on success, and a detailed error message otherwise.
"""
return login_user(request, api_version)
@@ -736,19 +765,19 @@ def dispatch(self, request, *args, **kwargs):
def _parse_analytics_param_for_course_id(request):
- """ If analytics request param is found, parse and add course id as a new request param. """
+ """If analytics request param is found, parse and add course id as a new request param."""
# Make a copy of the current POST request to modify.
modified_request = request.POST.copy()
if isinstance(request, HttpRequest):
# Works for an HttpRequest but not a rest_framework.request.Request.
# Note: This case seems to be used for tests only.
request.POST = modified_request
- set_custom_attribute('login_user_request_type', 'django')
+ set_custom_attribute("login_user_request_type", "django")
else:
# The request must be a rest_framework.request.Request.
# Note: Only DRF seems to be used in Production.
request._data = modified_request # pylint: disable=protected-access
- set_custom_attribute('login_user_request_type', 'drf')
+ set_custom_attribute("login_user_request_type", "drf")
# Include the course ID if it's specified in the analytics info
# so it can be included in analytics events.
@@ -758,9 +787,5 @@ def _parse_analytics_param_for_course_id(request):
if "enroll_course_id" in analytics:
modified_request["course_id"] = analytics.get("enroll_course_id")
except (ValueError, TypeError):
- set_custom_attribute('shim_analytics_course_id', 'parse-error')
- log.error(
- "Could not parse analytics object sent to user API: {analytics}".format(
- analytics=analytics
- )
- )
+ set_custom_attribute("shim_analytics_course_id", "parse-error")
+ log.error("Could not parse analytics object sent to user API: {analytics}".format(analytics=analytics))
diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt
index f2ef0216ca07..5188f37250ef 100644
--- a/requirements/common_constraints.txt
+++ b/requirements/common_constraints.txt
@@ -32,3 +32,7 @@ elasticsearch<7.14.0
# This can be unpinned once https://github.com/openedx/edx-platform/issues/34586
# has been resolved and edx-platform is running with pymongo>=4.4.0
+
+# Cause: https://github.com/openedx/edx-lint/issues/458
+# This can be unpinned once https://github.com/openedx/edx-lint/issues/459 has been resolved.
+pip<24.3
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 6c8fe968b230..0292e08f6189 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -82,7 +82,7 @@ django-storages<1.14.4
# The team that owns this package will manually bump this package rather than having it pulled in automatically.
# This is to allow them to better control its deployment and to do it in a process that works better
# for them.
-edx-enterprise==4.29.0
+edx-enterprise==4.30.0
# Date: 2024-05-09
# This has to be constrained as well because newer versions of edx-i18n-tools need the
diff --git a/requirements/edx-sandbox/base.txt b/requirements/edx-sandbox/base.txt
index 991ea0efcf6a..eb74898596ff 100644
--- a/requirements/edx-sandbox/base.txt
+++ b/requirements/edx-sandbox/base.txt
@@ -86,5 +86,5 @@ sympy==1.13.3
# via
# -r requirements/edx-sandbox/base.in
# openedx-calc
-tqdm==4.66.5
+tqdm==4.66.6
# via nltk
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 35106f149851..a0117e573084 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -70,13 +70,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/kernel.in
-boto3==1.35.46
+boto3==1.35.50
# via
# -r requirements/edx/kernel.in
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.46
+botocore==1.35.50
# via
# -r requirements/edx/kernel.in
# boto3
@@ -144,7 +144,7 @@ code-annotations==1.8.0
# edx-toggles
codejail-includes==1.0.0
# via -r requirements/edx/kernel.in
-crowdsourcehinter-xblock==0.7
+crowdsourcehinter-xblock==0.8
# via -r requirements/edx/bundled.in
cryptography==43.0.3
# via
@@ -455,7 +455,7 @@ edx-django-utils==7.0.0
# openedx-events
# ora2
# super-csv
-edx-drf-extensions==10.4.0
+edx-drf-extensions==10.5.0
# via
# -r requirements/edx/kernel.in
# edx-completion
@@ -467,11 +467,11 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.29.0
+edx-enterprise==4.30.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/kernel.in
-edx-event-bus-kafka==5.8.1
+edx-event-bus-kafka==6.0.0
# via -r requirements/edx/kernel.in
edx-event-bus-redis==0.5.1
# via -r requirements/edx/kernel.in
@@ -584,7 +584,7 @@ geoip2==4.8.0
# via -r requirements/edx/kernel.in
glob2==0.7
# via -r requirements/edx/kernel.in
-google-api-core[grpc]==2.21.0
+google-api-core[grpc]==2.22.0
# via
# firebase-admin
# google-api-python-client
@@ -747,7 +747,7 @@ markupsafe==3.0.2
# xblock
maxminddb==2.6.2
# via geoip2
-meilisearch==0.31.5
+meilisearch==0.31.6
# via
# -r requirements/edx/kernel.in
# edx-search
@@ -1038,9 +1038,9 @@ pyyaml==6.0.2
# xblock
random2==1.0.2
# via -r requirements/edx/kernel.in
-recommender-xblock==2.2.1
+recommender-xblock==3.0.0
# via -r requirements/edx/bundled.in
-redis==5.1.1
+redis==5.2.0
# via
# -r requirements/edx/kernel.in
# walrus
@@ -1144,7 +1144,7 @@ slumber==0.7.1
# -r requirements/edx/kernel.in
# edx-bulk-grades
# edx-enterprise
-snowflake-connector-python==3.12.2
+snowflake-connector-python==3.12.3
# via edx-enterprise
social-auth-app-django==5.4.1
# via
@@ -1191,7 +1191,7 @@ tinycss2==1.2.1
# via bleach
tomlkit==0.13.2
# via snowflake-connector-python
-tqdm==4.66.5
+tqdm==4.66.6
# via
# nltk
# openai
@@ -1254,7 +1254,7 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.8
+webob==1.8.9
# via
# -r requirements/edx/kernel.in
# xblock
@@ -1291,7 +1291,7 @@ xmlsec==1.3.13
# python3-saml
xss-utils==0.6.0
# via -r requirements/edx/kernel.in
-yarl==1.16.0
+yarl==1.17.0
# via aiohttp
zipp==3.20.2
# via importlib-metadata
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index b6a4b48d04af..485a57753a68 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -140,14 +140,14 @@ boto==2.49.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-boto3==1.35.46
+boto3==1.35.50
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.46
+botocore==1.35.50
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -282,7 +282,7 @@ coverage[toml]==7.6.4
# via
# -r requirements/edx/testing.txt
# pytest-cov
-crowdsourcehinter-xblock==0.7
+crowdsourcehinter-xblock==0.8
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -576,7 +576,7 @@ django-stubs==1.16.0
# -c requirements/edx/../constraints.txt
# -r requirements/edx/development.in
# djangorestframework-stubs
-django-stubs-ext==5.1.0
+django-stubs-ext==5.1.1
# via django-stubs
django-user-tasks==3.2.0
# via
@@ -728,7 +728,7 @@ edx-django-utils==7.0.0
# openedx-events
# ora2
# super-csv
-edx-drf-extensions==10.4.0
+edx-drf-extensions==10.5.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -741,12 +741,12 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.29.0
+edx-enterprise==4.30.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-edx-event-bus-kafka==5.8.1
+edx-event-bus-kafka==6.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -760,7 +760,7 @@ edx-i18n-tools==1.5.0
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
# ora2
-edx-lint==5.4.0
+edx-lint==5.4.1
# via -r requirements/edx/testing.txt
edx-milestones==0.6.0
# via
@@ -879,11 +879,11 @@ execnet==2.1.1
# pytest-xdist
factory-boy==3.3.1
# via -r requirements/edx/testing.txt
-faker==30.8.0
+faker==30.8.1
# via
# -r requirements/edx/testing.txt
# factory-boy
-fastapi==0.115.3
+fastapi==0.115.4
# via
# -r requirements/edx/testing.txt
# pact-python
@@ -943,7 +943,7 @@ glob2==0.7
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-google-api-core[grpc]==2.21.0
+google-api-core[grpc]==2.22.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1236,7 +1236,7 @@ mccabe==0.7.0
# via
# -r requirements/edx/testing.txt
# pylint
-meilisearch==0.31.5
+meilisearch==0.31.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1712,7 +1712,7 @@ pytest-metadata==1.8.0
# via
# -r requirements/edx/testing.txt
# pytest-json-report
-pytest-randomly==3.15.0
+pytest-randomly==3.16.0
# via -r requirements/edx/testing.txt
pytest-xdist[psutil]==3.6.1
# via -r requirements/edx/testing.txt
@@ -1800,11 +1800,11 @@ random2==1.0.2
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-recommender-xblock==2.2.1
+recommender-xblock==3.0.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-redis==5.1.1
+redis==5.2.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -1958,7 +1958,7 @@ snowballstemmer==2.2.0
# via
# -r requirements/edx/doc.txt
# sphinx
-snowflake-connector-python==3.12.2
+snowflake-connector-python==3.12.3
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2053,7 +2053,7 @@ staff-graded-xblock==2.3.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-starlette==0.41.0
+starlette==0.41.2
# via
# -r requirements/edx/testing.txt
# fastapi
@@ -2101,7 +2101,7 @@ tomlkit==0.13.2
# snowflake-connector-python
tox==4.23.2
# via -r requirements/edx/testing.txt
-tqdm==4.66.5
+tqdm==4.66.6
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2180,7 +2180,7 @@ vine==5.1.0
# amqp
# celery
# kombu
-virtualenv==20.27.0
+virtualenv==20.27.1
# via
# -r requirements/edx/testing.txt
# tox
@@ -2222,7 +2222,7 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.8
+webob==1.8.9
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
@@ -2280,7 +2280,7 @@ xss-utils==0.6.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
-yarl==1.16.0
+yarl==1.17.0
# via
# -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt
diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt
index e0f08601b03a..5f6f0e1162ad 100644
--- a/requirements/edx/doc.txt
+++ b/requirements/edx/doc.txt
@@ -102,13 +102,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.35.46
+boto3==1.35.50
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.46
+botocore==1.35.50
# via
# -r requirements/edx/base.txt
# boto3
@@ -194,7 +194,7 @@ code-annotations==1.8.0
# edx-toggles
codejail-includes==1.0.0
# via -r requirements/edx/base.txt
-crowdsourcehinter-xblock==0.7
+crowdsourcehinter-xblock==0.8
# via -r requirements/edx/base.txt
cryptography==43.0.3
# via
@@ -535,7 +535,7 @@ edx-django-utils==7.0.0
# openedx-events
# ora2
# super-csv
-edx-drf-extensions==10.4.0
+edx-drf-extensions==10.5.0
# via
# -r requirements/edx/base.txt
# edx-completion
@@ -547,11 +547,11 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.29.0
+edx-enterprise==4.30.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-edx-event-bus-kafka==5.8.1
+edx-event-bus-kafka==6.0.0
# via -r requirements/edx/base.txt
edx-event-bus-redis==0.5.1
# via -r requirements/edx/base.txt
@@ -683,7 +683,7 @@ gitpython==3.1.43
# via -r requirements/edx/doc.in
glob2==0.7
# via -r requirements/edx/base.txt
-google-api-core[grpc]==2.21.0
+google-api-core[grpc]==2.22.0
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -891,7 +891,7 @@ maxminddb==2.6.2
# via
# -r requirements/edx/base.txt
# geoip2
-meilisearch==0.31.5
+meilisearch==0.31.6
# via
# -r requirements/edx/base.txt
# edx-search
@@ -1247,9 +1247,9 @@ pyyaml==6.0.2
# xblock
random2==1.0.2
# via -r requirements/edx/base.txt
-recommender-xblock==2.2.1
+recommender-xblock==3.0.0
# via -r requirements/edx/base.txt
-redis==5.1.1
+redis==5.2.0
# via
# -r requirements/edx/base.txt
# walrus
@@ -1371,7 +1371,7 @@ smmap==5.0.1
# via gitdb
snowballstemmer==2.2.0
# via sphinx
-snowflake-connector-python==3.12.2
+snowflake-connector-python==3.12.3
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1472,7 +1472,7 @@ tomlkit==0.13.2
# via
# -r requirements/edx/base.txt
# snowflake-connector-python
-tqdm==4.66.5
+tqdm==4.66.6
# via
# -r requirements/edx/base.txt
# nltk
@@ -1547,7 +1547,7 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.8
+webob==1.8.9
# via
# -r requirements/edx/base.txt
# xblock
@@ -1586,7 +1586,7 @@ xmlsec==1.3.13
# python3-saml
xss-utils==0.6.0
# via -r requirements/edx/base.txt
-yarl==1.16.0
+yarl==1.17.0
# via
# -r requirements/edx/base.txt
# aiohttp
diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt
index 6ad814b602d6..61db06bbf123 100644
--- a/requirements/edx/semgrep.txt
+++ b/requirements/edx/semgrep.txt
@@ -116,7 +116,7 @@ ruamel-yaml==0.17.40
# via semgrep
ruamel-yaml-clib==0.2.12
# via ruamel-yaml
-semgrep==1.92.0
+semgrep==1.93.0
# via -r requirements/edx/semgrep.in
tomli==2.0.2
# via semgrep
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 9cd37f6e4b56..fc60c467d4ab 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -102,13 +102,13 @@ bleach[css]==6.1.0
# xblock-poll
boto==2.49.0
# via -r requirements/edx/base.txt
-boto3==1.35.46
+boto3==1.35.50
# via
# -r requirements/edx/base.txt
# django-ses
# fs-s3fs
# ora2
-botocore==1.35.46
+botocore==1.35.50
# via
# -r requirements/edx/base.txt
# boto3
@@ -213,7 +213,7 @@ coverage[toml]==7.6.4
# via
# -r requirements/edx/coverage.txt
# pytest-cov
-crowdsourcehinter-xblock==0.7
+crowdsourcehinter-xblock==0.8
# via -r requirements/edx/base.txt
cryptography==43.0.3
# via
@@ -559,7 +559,7 @@ edx-django-utils==7.0.0
# openedx-events
# ora2
# super-csv
-edx-drf-extensions==10.4.0
+edx-drf-extensions==10.5.0
# via
# -r requirements/edx/base.txt
# edx-completion
@@ -571,11 +571,11 @@ edx-drf-extensions==10.4.0
# edx-when
# edxval
# openedx-learning
-edx-enterprise==4.29.0
+edx-enterprise==4.30.0
# via
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
-edx-event-bus-kafka==5.8.1
+edx-event-bus-kafka==6.0.0
# via -r requirements/edx/base.txt
edx-event-bus-redis==0.5.1
# via -r requirements/edx/base.txt
@@ -584,7 +584,7 @@ edx-i18n-tools==1.5.0
# -c requirements/edx/../constraints.txt
# -r requirements/edx/base.txt
# ora2
-edx-lint==5.4.0
+edx-lint==5.4.1
# via -r requirements/edx/testing.in
edx-milestones==0.6.0
# via -r requirements/edx/base.txt
@@ -674,9 +674,9 @@ execnet==2.1.1
# via pytest-xdist
factory-boy==3.3.1
# via -r requirements/edx/testing.in
-faker==30.8.0
+faker==30.8.1
# via factory-boy
-fastapi==0.115.3
+fastapi==0.115.4
# via pact-python
fastavro==1.9.7
# via
@@ -717,7 +717,7 @@ geoip2==4.8.0
# via -r requirements/edx/base.txt
glob2==0.7
# via -r requirements/edx/base.txt
-google-api-core[grpc]==2.21.0
+google-api-core[grpc]==2.22.0
# via
# -r requirements/edx/base.txt
# firebase-admin
@@ -944,7 +944,7 @@ maxminddb==2.6.2
# geoip2
mccabe==0.7.0
# via pylint
-meilisearch==0.31.5
+meilisearch==0.31.6
# via
# -r requirements/edx/base.txt
# edx-search
@@ -1295,7 +1295,7 @@ pytest-metadata==1.8.0
# via
# -r requirements/edx/testing.in
# pytest-json-report
-pytest-randomly==3.15.0
+pytest-randomly==3.16.0
# via -r requirements/edx/testing.in
pytest-xdist[psutil]==3.6.1
# via -r requirements/edx/testing.in
@@ -1365,9 +1365,9 @@ pyyaml==6.0.2
# xblock
random2==1.0.2
# via -r requirements/edx/base.txt
-recommender-xblock==2.2.1
+recommender-xblock==3.0.0
# via -r requirements/edx/base.txt
-redis==5.1.1
+redis==5.2.0
# via
# -r requirements/edx/base.txt
# walrus
@@ -1490,7 +1490,7 @@ slumber==0.7.1
# edx-enterprise
sniffio==1.3.1
# via anyio
-snowflake-connector-python==3.12.2
+snowflake-connector-python==3.12.3
# via
# -r requirements/edx/base.txt
# edx-enterprise
@@ -1522,7 +1522,7 @@ sqlparse==0.5.1
# django
staff-graded-xblock==2.3.0
# via -r requirements/edx/base.txt
-starlette==0.41.0
+starlette==0.41.2
# via fastapi
stevedore==5.3.0
# via
@@ -1560,7 +1560,7 @@ tomlkit==0.13.2
# snowflake-connector-python
tox==4.23.2
# via -r requirements/edx/testing.in
-tqdm==4.66.5
+tqdm==4.66.6
# via
# -r requirements/edx/base.txt
# nltk
@@ -1614,7 +1614,7 @@ vine==5.1.0
# amqp
# celery
# kombu
-virtualenv==20.27.0
+virtualenv==20.27.1
# via tox
voluptuous==0.15.2
# via
@@ -1644,7 +1644,7 @@ webencodings==0.5.1
# bleach
# html5lib
# tinycss2
-webob==1.8.8
+webob==1.8.9
# via
# -r requirements/edx/base.txt
# xblock
@@ -1685,7 +1685,7 @@ xmlsec==1.3.13
# python3-saml
xss-utils==0.6.0
# via -r requirements/edx/base.txt
-yarl==1.16.0
+yarl==1.17.0
# via
# -r requirements/edx/base.txt
# aiohttp
diff --git a/requirements/pip.txt b/requirements/pip.txt
index 346a0611f0c5..797974efa45a 100644
--- a/requirements/pip.txt
+++ b/requirements/pip.txt
@@ -9,6 +9,8 @@ wheel==0.44.0
# The following packages are considered to be unsafe in a requirements file:
pip==24.2
- # via -r requirements/pip.in
+ # via
+ # -c requirements/common_constraints.txt
+ # -r requirements/pip.in
setuptools==75.2.0
# via -r requirements/pip.in
diff --git a/scripts/user_retirement/requirements/base.txt b/scripts/user_retirement/requirements/base.txt
index 5c2c300341a8..288d660b14be 100644
--- a/scripts/user_retirement/requirements/base.txt
+++ b/scripts/user_retirement/requirements/base.txt
@@ -10,9 +10,9 @@ attrs==24.2.0
# via zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.in
-boto3==1.35.46
+boto3==1.35.50
# via -r scripts/user_retirement/requirements/base.in
-botocore==1.35.46
+botocore==1.35.50
# via
# boto3
# s3transfer
@@ -50,7 +50,7 @@ edx-django-utils==7.0.0
# via edx-rest-api-client
edx-rest-api-client==6.0.0
# via -r scripts/user_retirement/requirements/base.in
-google-api-core==2.21.0
+google-api-core==2.22.0
# via google-api-python-client
google-api-python-client==2.149.0
# via -r scripts/user_retirement/requirements/base.in
diff --git a/scripts/user_retirement/requirements/testing.txt b/scripts/user_retirement/requirements/testing.txt
index a149a1d10cd1..da4ce1b0391a 100644
--- a/scripts/user_retirement/requirements/testing.txt
+++ b/scripts/user_retirement/requirements/testing.txt
@@ -14,11 +14,11 @@ attrs==24.2.0
# zeep
backoff==2.2.1
# via -r scripts/user_retirement/requirements/base.txt
-boto3==1.35.46
+boto3==1.35.50
# via
# -r scripts/user_retirement/requirements/base.txt
# moto
-botocore==1.35.46
+botocore==1.35.50
# via
# -r scripts/user_retirement/requirements/base.txt
# boto3
@@ -72,7 +72,7 @@ edx-django-utils==7.0.0
# edx-rest-api-client
edx-rest-api-client==6.0.0
# via -r scripts/user_retirement/requirements/base.txt
-google-api-core==2.21.0
+google-api-core==2.22.0
# via
# -r scripts/user_retirement/requirements/base.txt
# google-api-python-client
@@ -272,7 +272,7 @@ urllib3==1.26.20
# botocore
# requests
# responses
-werkzeug==3.0.4
+werkzeug==3.0.6
# via moto
xmltodict==0.14.2
# via moto
diff --git a/xmodule/annotatable_block.py b/xmodule/annotatable_block.py
index e41e2b17a52f..cec677f6c5d5 100644
--- a/xmodule/annotatable_block.py
+++ b/xmodule/annotatable_block.py
@@ -11,7 +11,7 @@
from openedx.core.djangolib.markup import HTML, Text
from xmodule.editing_block import EditingMixin
from xmodule.raw_block import RawMixin
-from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment
+from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment
from xmodule.xml_block import XmlMixin
from xmodule.x_module import (
ResourceTemplates,
@@ -180,7 +180,7 @@ def student_view(self, context): # lint-amnesty, pylint: disable=unused-argumen
"""
fragment = Fragment()
fragment.add_content(self.get_html())
- add_sass_to_fragment(fragment, 'AnnotatableBlockDisplay.scss')
+ add_css_to_fragment(fragment, 'AnnotatableBlockDisplay.css')
add_webpack_js_to_fragment(fragment, 'AnnotatableBlockDisplay')
shim_xmodule_js(fragment, 'Annotatable')
@@ -193,7 +193,7 @@ def studio_view(self, _context):
fragment = Fragment(
self.runtime.service(self, 'mako').render_cms_template(self.mako_template, self.get_context())
)
- add_sass_to_fragment(fragment, 'AnnotatableBlockEditor.scss')
+ add_css_to_fragment(fragment, 'AnnotatableBlockEditor.css')
add_webpack_js_to_fragment(fragment, 'AnnotatableBlockEditor')
shim_xmodule_js(fragment, self.studio_js_module_name)
return fragment
diff --git a/xmodule/assets/AnnotatableBlockDisplay.scss b/xmodule/assets/AnnotatableBlockDisplay.scss
deleted file mode 100644
index 66e1e756f3da..000000000000
--- a/xmodule/assets/AnnotatableBlockDisplay.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.xmodule_display.xmodule_AnnotatableBlock {
- @import "annotatable/display.scss";
-}
diff --git a/xmodule/assets/AnnotatableBlockEditor.scss b/xmodule/assets/AnnotatableBlockEditor.scss
deleted file mode 100644
index 8f2852422d7d..000000000000
--- a/xmodule/assets/AnnotatableBlockEditor.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.xmodule_edit.xmodule_AnnotatableBlock {
- @import "codemirror/codemirror.scss";
-}
diff --git a/xmodule/assets/WordCloudBlockDisplay.scss b/xmodule/assets/WordCloudBlockDisplay.scss
deleted file mode 100644
index 884112a4804c..000000000000
--- a/xmodule/assets/WordCloudBlockDisplay.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.xmodule_display.xmodule_WordCloudBlock {
- @import "word_cloud/display.scss";
-}
diff --git a/xmodule/assets/annotatable/_display.scss b/xmodule/assets/annotatable/_display.scss
deleted file mode 100644
index 9aaa8649c6c5..000000000000
--- a/xmodule/assets/annotatable/_display.scss
+++ /dev/null
@@ -1,197 +0,0 @@
-/* TODO: move top-level variables to a common _variables.scss.
- * NOTE: These variables were only added here because when this was integrated with the CMS,
- * SASS compilation errors were triggered because the CMS didn't have the same variables defined
- * that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS.
- * -Abarrett and Vshnayder
- */
-@import 'bourbon/bourbon';
-@import 'lms/theme/variables';
-@import 'bootstrap/scss/variables';
-@import 'lms/theme/variables-v1';
-
-$annotatable--border-color: var(--gray-l3);
-$annotatable--body-font-size: em(14);
-
-.annotatable-wrapper {
- position: relative;
-}
-
-.annotatable-header {
- margin-bottom: 0.5em;
-}
-
-.annotatable-section {
- position: relative;
- padding: 0.5em 1em;
- border: 1px solid $annotatable--border-color;
- border-radius: 0.5em;
- margin-bottom: 0.5em;
-
- &.shaded { background-color: #ededed; }
-
- .annotatable-section-title {
- font-weight: bold;
- a { font-weight: normal; }
- }
-
- .annotatable-section-body {
- border-top: 1px solid $annotatable--border-color;
- margin-top: 0.5em;
- padding-top: 0.5em;
-
- @include clearfix();
- }
-
- ul.instructions-template {
- list-style: disc;
- margin-left: 4em;
- b { font-weight: bold; }
- i { font-style: italic; }
-
- code {
- display: inline;
- white-space: pre;
- font-family: Courier New, monospace;
- }
- }
-}
-
-.annotatable-toggle {
- position: absolute;
- right: 0;
- margin: 2px 1em 2px 0;
- &.expanded::after { content: " \2191"; }
- &.collapsed::after { content: " \2193"; }
-}
-
-.annotatable-span {
- @extend %ui-fake-link;
-
- display: inline;
-
- $highlight_index: 0;
-
- @each $highlight in (
- (yellow rgba(255, 255,10, 0.3) rgba(255, 255,10, 0.9)),
- (red rgba(178,19,16,0.3) rgba(178,19,16,0.9)),
- (orange rgba(255,165,0, 0.3) rgba(255,165,0, 0.9)),
- (green rgba(25,255,132,0.3) rgba(25,255,132,0.9)),
- (blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)),
- (purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
-
- $highlight_index: $highlight_index + 1;
- $marker: nth($highlight, 1);
- $color: nth($highlight, 2);
- $selected_color: nth($highlight, 3);
-
- @if $highlight_index == 1 {
- &.highlight {
- background-color: $color;
- &.selected { background-color: $selected_color; }
- }
- }
-
- &.highlight-#{$marker} {
- background-color: $color;
- &.selected { background-color: $selected_color; }
- }
- }
-
- &.hide {
- cursor: none;
- background-color: inherit;
-
- .annotatable-icon {
- display: none;
- }
- }
-
- .annotatable-comment {
- display: none;
- }
-}
-
-.ui-tooltip.qtip.ui-tooltip {
- font-size: $annotatable--body-font-size;
- border: 1px solid #333;
- border-radius: 1em;
- background-color: rgba(0, 0, 0, 0.85);
- color: var(--white);
- -webkit-font-smoothing: antialiased;
-
- .ui-tooltip-titlebar {
- font-size: em(16);
- color: inherit;
- background-color: transparent;
- padding: calc((var(--baseline)/4)) calc((var(--baseline)/2));
- border: none;
-
- .ui-tooltip-title {
- padding: calc((var(--baseline)/4)) 0;
- border-bottom: 2px solid #333;
- font-weight: bold;
- }
-
- .ui-tooltip-icon {
- right: 10px;
- background: #333;
- }
-
- .ui-state-hover {
- color: inherit;
- border: 1px solid var(--gray-l3);
- }
- }
-
- .ui-tooltip-content {
- color: inherit;
- font-size: em(14);
- text-align: left;
- font-weight: 400;
- padding: 0 calc((var(--baseline)/2)) calc((var(--baseline)/2)) calc((var(--baseline)/2));
- background-color: transparent;
- border-color: transparent;
- }
-
- p {
- color: inherit;
- line-height: normal;
- }
-}
-
-.ui-tooltip.qtip.ui-tooltip-annotatable {
- max-width: 375px;
-
- .ui-tooltip-content {
- padding: 0 calc((var(--baseline)/2));
-
- .annotatable-comment {
- display: block;
- margin: 0 0 calc((var(--baseline)/2)) 0;
- max-height: 225px;
- overflow: auto;
- line-height: normal;
- }
-
- .annotatable-reply {
- display: block;
- border-top: 2px solid #333;
- padding: calc((var(--baseline)/4)) 0;
- margin: 0;
- text-align: center;
- }
- }
-
- &::after {
- content: '';
- display: inline-block;
- position: absolute;
- bottom: -20px;
- left: 50%;
- height: 0;
- width: 0;
- margin-left: calc(-1 * (var(--baseline) / 4));
- border: 10px solid transparent;
- border-top-color: rgba(0, 0, 0, 0.85);
- }
-}
diff --git a/xmodule/assets/word_cloud/_display.scss b/xmodule/assets/word_cloud/_display.scss
deleted file mode 100644
index 4f7320380a12..000000000000
--- a/xmodule/assets/word_cloud/_display.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-@import 'bourbon/bourbon';
-@import 'lms/theme/variables';
-@import 'bootstrap/scss/variables';
-@import 'lms/theme/variables-v1';
-
-.input-cloud {
- margin: calc((var(--baseline)/4));
-}
-
-.result_cloud_section {
- display: none;
- width: 0px;
- height: 0px;
-}
-
-.result_cloud_section.active {
- display: block;
- width: 100%;
- height: auto;
- margin-top: 1em;
-
- h3 {
- font-size: 100%;
- }
-}
-
-.your_words{
- font-size: 0.85em;
- display: block;
-}
diff --git a/xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css b/xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css
new file mode 100644
index 000000000000..45b395ec66ab
--- /dev/null
+++ b/xmodule/static/css-builtin-blocks/AnnotatableBlockDisplay.css
@@ -0,0 +1,243 @@
+@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700");
+
+.xmodule_display.xmodule_AnnotatableBlock {
+ /* TODO: move top-level variables to a common _variables.scss.
+ * NOTE: These variables were only added here because when this was integrated with the CMS,
+ * SASS compilation errors were triggered because the CMS didn't have the same variables defined
+ * that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS.
+ * -Abarrett and Vshnayder
+ */
+ /* stylelint-disable-line */
+ /* stylelint-disable-line */
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-wrapper {
+ position: relative;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-header {
+ margin-bottom: 0.5em;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section {
+ position: relative;
+ padding: 0.5em 1em;
+ border: 1px solid var(--gray-l3);
+ border-radius: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section.shaded {
+ background-color: #ededed;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-title {
+ font-weight: bold;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-title a {
+ font-weight: normal;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-body {
+ border-top: 1px solid var(--gray-l3);
+ margin-top: 0.5em;
+ padding-top: 0.5em;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section .annotatable-section-body:after {
+ content: "";
+ display: table;
+ clear: both;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template {
+ list-style: disc;
+ margin-left: 4em;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template b {
+ font-weight: bold;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template i {
+ font-style: italic;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-section ul.instructions-template code {
+ display: inline;
+ white-space: pre;
+ font-family: Courier New, monospace;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle {
+ position: absolute;
+ right: 0;
+ margin: 2px 1em 2px 0;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle.expanded::after {
+ content: " \2191";
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-toggle.collapsed::after {
+ content: " \2193";
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span {
+ display: inline;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight {
+ background-color: rgba(255, 255, 10, 0.3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight.selected {
+ background-color: rgba(255, 255, 10, 0.9);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-yellow {
+ background-color: rgba(255, 255, 10, 0.3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-yellow.selected {
+ background-color: rgba(255, 255, 10, 0.9);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-red {
+ background-color: rgba(178, 19, 16, 0.3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-red.selected {
+ background-color: rgba(178, 19, 16, 0.9);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-orange {
+ background-color: rgba(255, 165, 0, 0.3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-orange.selected {
+ background-color: rgba(255, 165, 0, 0.9);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-green {
+ background-color: rgba(25, 255, 132, 0.3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-green.selected {
+ background-color: rgba(25, 255, 132, 0.9);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-blue {
+ background-color: rgba(35, 163, 255, 0.3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-blue.selected {
+ background-color: rgba(35, 163, 255, 0.9);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-purple {
+ background-color: rgba(115, 9, 178, 0.3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.highlight-purple.selected {
+ background-color: rgba(115, 9, 178, 0.9);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.hide {
+ cursor: none;
+ background-color: inherit;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span.hide .annotatable-icon {
+ display: none;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .annotatable-span .annotatable-comment {
+ display: none;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip {
+ font-size: 0.875em;
+ border: 1px solid #333;
+ border-radius: 1em;
+ background-color: rgba(0, 0, 0, 0.85);
+ color: var(--white);
+ -webkit-font-smoothing: antialiased;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar {
+ font-size: 1em;
+ color: inherit;
+ background-color: transparent;
+ padding: calc((var(--baseline) / 4)) calc((var(--baseline) / 2));
+ border: none;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-title {
+ padding: calc((var(--baseline) / 4)) 0;
+ border-bottom: 2px solid #333;
+ font-weight: bold;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-tooltip-icon {
+ right: 10px;
+ background: #333;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-titlebar .ui-state-hover {
+ color: inherit;
+ border: 1px solid var(--gray-l3);
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip .ui-tooltip-content {
+ color: inherit;
+ font-size: 0.875em;
+ text-align: left;
+ font-weight: 400;
+ padding: 0 calc((var(--baseline) / 2)) calc((var(--baseline) / 2)) calc((var(--baseline) / 2));
+ background-color: transparent;
+ border-color: transparent;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip p {
+ color: inherit;
+ line-height: normal;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable {
+ max-width: 375px;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content {
+ padding: 0 calc((var(--baseline) / 2));
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-comment {
+ display: block;
+ margin: 0 0 calc((var(--baseline) / 2)) 0;
+ max-height: 225px;
+ overflow: auto;
+ line-height: normal;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable .ui-tooltip-content .annotatable-reply {
+ display: block;
+ border-top: 2px solid #333;
+ padding: calc((var(--baseline) / 4)) 0;
+ margin: 0;
+ text-align: center;
+}
+
+.xmodule_display.xmodule_AnnotatableBlock .ui-tooltip.qtip.ui-tooltip-annotatable::after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ bottom: -20px;
+ left: 50%;
+ height: 0;
+ width: 0;
+ margin-left: calc(-1 * (var(--baseline) / 4));
+ border: 10px solid transparent;
+ border-top-color: rgba(0, 0, 0, 0.85);
+}
diff --git a/xmodule/static/css-builtin-blocks/AnnotatableBlockEditor.css b/xmodule/static/css-builtin-blocks/AnnotatableBlockEditor.css
new file mode 100644
index 000000000000..498cbda9ffc4
--- /dev/null
+++ b/xmodule/static/css-builtin-blocks/AnnotatableBlockEditor.css
@@ -0,0 +1,5 @@
+.xmodule_edit.xmodule_AnnotatableBlock .CodeMirror {
+ background: #fff;
+ font-size: 13px;
+ color: #3c3c3c;
+}
diff --git a/xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css b/xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css
new file mode 100644
index 000000000000..51050dcba9a1
--- /dev/null
+++ b/xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css
@@ -0,0 +1,32 @@
+@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700");
+
+.xmodule_display.xmodule_WordCloudBlock {
+ /* stylelint-disable-line */
+ /* stylelint-disable-line */
+}
+
+.xmodule_display.xmodule_WordCloudBlock .input-cloud {
+ margin: calc((var(--baseline) / 4));
+}
+
+.xmodule_display.xmodule_WordCloudBlock .result_cloud_section {
+ display: none;
+ width: 0px;
+ height: 0px;
+}
+
+.xmodule_display.xmodule_WordCloudBlock .result_cloud_section.active {
+ display: block;
+ width: 100%;
+ height: auto;
+ margin-top: 1em;
+}
+
+.xmodule_display.xmodule_WordCloudBlock .result_cloud_section.active h3 {
+ font-size: 100%;
+}
+
+.xmodule_display.xmodule_WordCloudBlock .your_words {
+ font-size: 0.85em;
+ display: block;
+}
diff --git a/xmodule/word_cloud_block.py b/xmodule/word_cloud_block.py
index 26a2c38c5cfc..d678f2a9a9f5 100644
--- a/xmodule/word_cloud_block.py
+++ b/xmodule/word_cloud_block.py
@@ -15,7 +15,7 @@
from xblock.fields import Boolean, Dict, Integer, List, Scope, String
from xmodule.editing_block import EditingMixin
from xmodule.raw_block import EmptyDataRawMixin
-from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_sass_to_fragment
+from xmodule.util.builtin_assets import add_webpack_js_to_fragment, add_css_to_fragment
from xmodule.xml_block import XmlMixin
from xmodule.x_module import (
ResourceTemplates,
@@ -262,7 +262,7 @@ def student_view(self, context): # lint-amnesty, pylint: disable=unused-argumen
'num_inputs': self.num_inputs,
'submitted': self.submitted,
}))
- add_sass_to_fragment(fragment, 'WordCloudBlockDisplay.scss')
+ add_css_to_fragment(fragment, 'WordCloudBlockDisplay.css')
add_webpack_js_to_fragment(fragment, 'WordCloudBlockDisplay')
shim_xmodule_js(fragment, 'WordCloud')