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> &nbsp;</p>
"\ + "<p>abcd</p>
"\ + "<p>&nbsp;</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')