From e807f3e9aadd205a666d4bd3950d709121ba13ee Mon Sep 17 00:00:00 2001 From: Awais Qureshi Date: Thu, 12 Sep 2024 15:48:39 +0500 Subject: [PATCH 1/7] feat: upgrade change_due date to drf ( 16th ) (#35392) * feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/tests/test_api.py | 15 +++++ lms/djangoapps/instructor/views/api.py | 60 +++++++++++++------ lms/djangoapps/instructor/views/api_urls.py | 2 +- lms/djangoapps/instructor/views/serializer.py | 29 +++++++++ 4 files changed, 86 insertions(+), 20 deletions(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 0042b42c7b46..6e0a2545f530 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -4160,6 +4160,21 @@ def test_change_due_date(self): # This operation regenerates the cache, so we can use cached results from edx-when. assert get_date_for_block(self.course, self.week1, self.user1, use_cached=True) == due_date + def test_change_due_date_with_reason(self): + url = reverse('change_due_date', kwargs={'course_id': str(self.course.id)}) + due_date = datetime.datetime(2013, 12, 30, tzinfo=UTC) + response = self.client.post(url, { + 'student': self.user1.username, + 'url': str(self.week1.location), + 'due_datetime': '12/30/2013 00:00', + 'reason': 'Testing reason.' # this is optional field. + }) + assert response.status_code == 200, response.content + + assert get_extended_due(self.course, self.week1, self.user1) == due_date + # This operation regenerates the cache, so we can use cached results from edx-when. + assert get_date_for_block(self.course, self.week1, self.user1, use_cached=True) == due_date + def test_change_to_invalid_due_date(self): url = reverse('change_due_date', kwargs={'course_id': str(self.course.id)}) response = self.client.post(url, { diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 09a2b9ffd140..d42e7173b0bf 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -107,8 +107,8 @@ from lms.djangoapps.instructor_task.data import InstructorTaskTypes from lms.djangoapps.instructor_task.models import ReportStore from lms.djangoapps.instructor.views.serializer import ( - AccessSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, - UserSerializer, SendEmailSerializer, StudentAttemptsSerializer + AccessSerializer, BlockDueDateSerializer, RoleNameSerializer, ShowStudentExtensionSerializer, UserSerializer, + SendEmailSerializer, StudentAttemptsSerializer ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted @@ -2967,28 +2967,50 @@ def _display_unit(unit): return str(unit.location) -@handle_dashboard_error -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) -@require_post_params('student', 'url', 'due_datetime') -def change_due_date(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ChangeDueDate(APIView): """ Grants a due date extension to a student for a particular unit. """ - course = get_course_by_id(CourseKey.from_string(course_id)) - student = require_student_from_identifier(request.POST.get('student')) - unit = find_unit(course, request.POST.get('url')) - due_date = parse_datetime(request.POST.get('due_datetime')) - reason = strip_tags(request.POST.get('reason', '')) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.GIVE_STUDENT_EXTENSION + serializer_class = BlockDueDateSerializer - set_due_date_extension(course, unit, student, due_date, request.user, reason=reason) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + Grants a due date extension to a student for a particular unit. - return JsonResponse(_( - 'Successfully changed due date for student {0} for {1} ' - 'to {2}').format(student.profile.name, _display_unit(unit), - due_date.strftime('%Y-%m-%d %H:%M'))) + params: + url (str): The URL related to the block that needs the due date update. + due_datetime (str): The new due date and time for the block. + student (str): The email or username of the student whose access is being modified. + """ + serializer_data = self.serializer_class(data=request.data) + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) + + student = serializer_data.validated_data.get('student') + if not student: + response_payload = { + 'error': f'Could not find student matching identifier: {request.data.get("student")}' + } + return JsonResponse(response_payload) + + course = get_course_by_id(CourseKey.from_string(course_id)) + + unit = find_unit(course, serializer_data.validated_data.get('url')) + due_date = parse_datetime(serializer_data.validated_data.get('due_datetime')) + reason = strip_tags(serializer_data.validated_data.get('reason', '')) + try: + set_due_date_extension(course, unit, student, due_date, request.user, reason=reason) + except Exception as error: # pylint: disable=broad-except + return JsonResponse({'error': str(error)}, status=400) + + return JsonResponse(_( + 'Successfully changed due date for student {0} for {1} ' + 'to {2}').format(student.profile.name, _display_unit(unit), + due_date.strftime('%Y-%m-%d %H:%M'))) @handle_dashboard_error diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index b54d38d80451..0cb80238f7c2 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -49,8 +49,8 @@ path('list_email_content', api.ListEmailContent.as_view(), name='list_email_content'), path('list_forum_members', api.list_forum_members, name='list_forum_members'), path('update_forum_role_membership', api.update_forum_role_membership, name='update_forum_role_membership'), + path('change_due_date', api.ChangeDueDate.as_view(), name='change_due_date'), path('send_email', api.SendEmail.as_view(), name='send_email'), - path('change_due_date', api.change_due_date, name='change_due_date'), path('reset_due_date', api.reset_due_date, name='reset_due_date'), path('show_unit_extensions', api.show_unit_extensions, name='show_unit_extensions'), path('show_student_extensions', api.ShowStudentExtensions.as_view(), name='show_student_extensions'), diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py index d4a3d4e8328a..793acc9c6137 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -149,3 +149,32 @@ class SendEmailSerializer(serializers.Serializer): subject = serializers.CharField(max_length=128, write_only=True, required=True) message = serializers.CharField(required=True) schedule = serializers.CharField(required=False) + + +class BlockDueDateSerializer(serializers.Serializer): + """ + Serializer for handling block due date updates for a specific student. + Fields: + url (str): The URL related to the block that needs the due date update. + due_datetime (str): The new due date and time for the block. + student (str): The email or username of the student whose access is being modified. + reason (str): Reason why updating this. + """ + url = serializers.CharField() + due_datetime = serializers.CharField() + student = serializers.CharField( + max_length=255, + help_text="Email or username of user to change access" + ) + reason = serializers.CharField(required=False) + + def validate_student(self, value): + """ + Validate that the student corresponds to an existing user. + """ + try: + user = get_student_from_identifier(value) + except User.DoesNotExist: + return None + + return user From d4b663cbc36ccfb856d2f0d00cfbc9eda6de46cd Mon Sep 17 00:00:00 2001 From: Prashant Makwana Date: Thu, 12 Sep 2024 11:17:55 -0400 Subject: [PATCH 2/7] chore: bumping enterprise version to 4.25.10 (#35478) --- requirements/constraints.txt | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index b5d838156124..8c95f7fcc200 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -26,7 +26,7 @@ celery>=5.2.2,<6.0.0 # 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.25.9 +edx-enterprise==4.25.10 # Stay on LTS version, remove once this is added to common constraint Django<5.0 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 40d64855cb52..ae29748fa5ae 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -467,7 +467,7 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.25.9 +edx-enterprise==4.25.10 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index a18ddb26b8b9..d4946b8f809b 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -741,7 +741,7 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.25.9 +edx-enterprise==4.25.10 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 00fc580dedc4..e3d2197cae86 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -547,7 +547,7 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.25.9 +edx-enterprise==4.25.10 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 966bd772a876..1407505d6ade 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -571,7 +571,7 @@ edx-drf-extensions==10.3.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.25.9 +edx-enterprise==4.25.10 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt From 448edd7e5fd4fd24181eb86cf639f2fb2269fc33 Mon Sep 17 00:00:00 2001 From: Demid Date: Thu, 12 Sep 2024 19:36:35 +0300 Subject: [PATCH 3/7] feat: add can_access_advanced_settings to studio home (#35426) --- cms/djangoapps/contentstore/rest_api/v1/serializers/home.py | 1 + cms/djangoapps/contentstore/rest_api/v1/views/home.py | 1 + cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py | 1 + cms/djangoapps/contentstore/utils.py | 1 + 4 files changed, 4 insertions(+) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index 1afc51ed77af..0aa06d8b8dcc 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -51,6 +51,7 @@ class CourseHomeSerializer(serializers.Serializer): allow_empty=True ) archived_courses = CourseCommonSerializer(required=False, many=True) + can_access_advanced_settings = serializers.BooleanField() can_create_organizations = serializers.BooleanField() course_creator_status = serializers.CharField() courses = CourseCommonSerializer(required=False, many=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index d41ceb2647c5..d72042cff611 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -52,6 +52,7 @@ def get(self, request: Request): "allow_unicode_course_id": false, "allowed_organizations": [], "archived_courses": [], + "can_access_advanced_settings": true, "can_create_organizations": true, "course_creator_status": "granted", "courses": [], diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py index 1b8bfaa84728..a8b4cf5e3933 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_home.py @@ -44,6 +44,7 @@ def test_home_page_courses_response(self): "allow_unicode_course_id": False, "allowed_organizations": [], "archived_courses": [], + "can_access_advanced_settings": True, "can_create_organizations": True, "course_creator_status": "granted", "courses": [], diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index dd00f245ef55..214193918eb4 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -1716,6 +1716,7 @@ def get_home_context(request, no_course=False): 'allowed_organizations': get_allowed_organizations(user), 'allowed_organizations_for_libraries': get_allowed_organizations_for_libraries(user), 'can_create_organizations': user_can_create_organizations(user), + 'can_access_advanced_settings': auth.has_studio_advanced_settings_access(user), } return home_context From 3adfb783363008fd3a0455e79f9e9bdc0b1d4594 Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Thu, 12 Sep 2024 12:40:30 -0400 Subject: [PATCH 4/7] revert: refactor: Convert builtin blocks' sass variables to css variables (#35480) This reverts commit 082350e72a4d62a79634ca87b5f8a2b1d4cf39a8. --- .../static/sass/_builtin-block-variables.scss | 10 +- xmodule/assets/capa/_display.scss | 352 +++++++++--------- xmodule/assets/editor/_edit.scss | 12 +- xmodule/assets/html/_display.scss | 44 +-- xmodule/assets/lti/_lti.scss | 10 +- xmodule/assets/poll/_display.scss | 16 +- xmodule/assets/problem/_edit.scss | 18 +- xmodule/assets/sequence/_display.scss | 14 +- xmodule/assets/tabs/_codemirror.scss | 2 +- xmodule/assets/tabs/_tabs.scss | 40 +- xmodule/assets/video/_accessible_menu.scss | 47 ++- xmodule/assets/video/_display.scss | 92 ++--- 12 files changed, 324 insertions(+), 333 deletions(-) diff --git a/common/static/sass/_builtin-block-variables.scss b/common/static/sass/_builtin-block-variables.scss index e232cb57d575..2c567c6fb1f4 100644 --- a/common/static/sass/_builtin-block-variables.scss +++ b/common/static/sass/_builtin-block-variables.scss @@ -16,6 +16,7 @@ :root { --action-primary-active-bg: $action-primary-active-bg; + --all-text-inputs: $all-text-inputs; --base-font-size: $base-font-size; --base-line-height: $base-line-height; --baseline: $baseline; @@ -25,7 +26,6 @@ --blue-d1: $blue-d1; --blue-d2: $blue-d2; --blue-d4: $blue-d4; - --blue-s1: $blue-s1; --body-color: $body-color; --border-color: $border-color; --bp-screen-lg: $bp-screen-lg; @@ -34,8 +34,6 @@ --danger: $danger; --darkGrey: $darkGrey; --error-color: $error-color; - --error-color-dark: darken($error-color, 11%); - --error-color-light: lighten($error-color, 25%); --font-bold: $font-bold; --font-family-sans-serif: $font-family-sans-serif; --general-color-accent: $general-color-accent; @@ -46,12 +44,6 @@ --gray-l3: $gray-l3; --gray-l4: $gray-l4; --gray-l6: $gray-l6; - --icon-correct: url($static-path + '/images/correct-icon.png'); - --icon-incorrect: url($static-path + '/images/incorrect-icon.png'); - --icon-info: url($static-path + '/images/info-icon.png'); - --icon-partially-correct: url($static-path + '/images/partially-correct-icon.png'); - --icon-spinner: url($static-path + '/images/spinner.gif'); - --icon-unanswered: url($static-path + '/images/unanswered-icon.png'); --incorrect: $incorrect; --lightGrey: $lightGrey; --lighter-base-font-color: $lighter-base-font-color; diff --git a/xmodule/assets/capa/_display.scss b/xmodule/assets/capa/_display.scss index 0aba39f89925..15571b65dc30 100644 --- a/xmodule/assets/capa/_display.scss +++ b/xmodule/assets/capa/_display.scss @@ -50,7 +50,7 @@ $asterisk-icon: '\f069'; // .fa-asterisk // +Mixins - Status Icon - Capa // ==================== -@mixin status-icon($color: var(--gray), $fontAwesomeIcon: "\f00d") { +@mixin status-icon($color: $gray, $fontAwesomeIcon: "\f00d") { .status-icon { &::after { @extend %use-font-awesome; @@ -66,13 +66,13 @@ $asterisk-icon: '\f069'; // .fa-asterisk // ==================== h2 { margin-top: 0; - margin-bottom: calc((var(--baseline)*0.75)); + margin-bottom: ($baseline*0.75); &.problem-header { display: inline-block; section.staff { - margin-top: calc((var(--baseline)*1.5)); + margin-top: ($baseline*1.5); font-size: 80%; } } @@ -89,10 +89,10 @@ h2 { } %feedback-hint { - margin-top: calc((var(--baseline) / 4)); + margin-top: ($baseline / 4); .icon { - @include margin-right(calc((var(--baseline) / 4))); + @include margin-right($baseline / 4); } } @@ -100,7 +100,7 @@ h2 { @extend %feedback-hint; .icon { - color: var(--incorrect); + color: $incorrect; } } @@ -109,7 +109,7 @@ h2 { @extend %feedback-hint; .icon { - color: var(--correct); + color: $correct; } } @@ -143,19 +143,19 @@ iframe[seamless] { } .inline-error { - color: var(--error-color-dark); + color: darken($error-color, 11%); } div.problem-progress { display: inline-block; - color: var(--gray-d1); + color: $gray-d1; font-size: em(14); } // +Problem - Base // ==================== div.problem { - padding-top: var(--baseline); + padding-top: $baseline; @media print { display: block; @@ -176,25 +176,25 @@ div.problem { display: inline; + p { - margin-top: var(--baseline); + margin-top: $baseline; } } .question-description { - color: var(--gray-d1); - font-size: var(--small-font-size); + color: $gray-d1; + font-size: $small-font-size; } form > label, .problem-group-label { display: block; - margin-bottom: var(--baseline); + margin-bottom: $baseline; font: inherit; color: inherit; -webkit-font-smoothing: initial; } .problem-group-label + .question-description { - margin-top: calc(-1 * var(--baseline)); + margin-top: -$baseline; } } @@ -203,7 +203,7 @@ div.problem { // can not use the & + & since .problem is nested deeply in .xmodule_display.xmodule_CapaModule .wrapper-problem-response + .wrapper-problem-response, .wrapper-problem-response + p { - margin-top: calc((var(--baseline) * 1.5)); + margin-top: ($baseline * 1.5); } // Choice Group - silent class @@ -219,14 +219,14 @@ div.problem { display: inline-block; clear: both; - margin-bottom: calc((var(--baseline)/2)); - border: 2px solid var(--gray-l4); + margin-bottom: ($baseline/2); + border: 2px solid $gray-l4; border-radius: 3px; - padding: calc((var(--baseline)/2)); + padding: ($baseline/2); width: 100%; &::after { - @include margin-left(calc((var(--baseline)*0.75))); + @include margin-left($baseline*0.75); } } @@ -242,15 +242,15 @@ div.problem { input[type="radio"], input[type="checkbox"] { - @include margin(calc((var(--baseline)/4))); - @include margin-right(calc((var(--baseline)/2))); + @include margin($baseline/4); + @include margin-right($baseline/2); } input { &:focus, &:hover { & + label { - border: 2px solid var(--blue); + border: 2px solid $blue; } } @@ -258,25 +258,25 @@ div.problem { &:focus, &:hover { & + label.choicegroup_correct { - @include status-icon(var(--correct), $checkmark-icon); + @include status-icon($correct, $checkmark-icon); - border: 2px solid var(--correct); + border: 2px solid $correct; } & + label.choicegroup_partially-correct { - @include status-icon(var(--partially-correct), $asterisk-icon); + @include status-icon($partially-correct, $asterisk-icon); - border: 2px solid var(--partially-correct); + border: 2px solid $partially-correct; } & + label.choicegroup_incorrect { - @include status-icon(var(--incorrect), $cross-icon); + @include status-icon($incorrect, $cross-icon); - border: 2px solid var(--incorrect); + border: 2px solid $incorrect; } & + label.choicegroup_submitted { - border: 2px solid var(--submitted); + border: 2px solid $submitted; } } } @@ -293,11 +293,11 @@ div.problem { } label { - @include padding(calc((var(--baseline)/2))); - @include padding-left(calc((var(--baseline)*2.3))); + @include padding($baseline/2); + @include padding-left($baseline*2.3); position: relative; - font-size: var(--base-font-size); + font-size: $base-font-size; line-height: normal; cursor: pointer; } @@ -308,19 +308,19 @@ div.problem { position: absolute; top: 0.35em; - width: calc(var(--baseline)*1.1); - height: calc(var(--baseline)*1.1); + width: $baseline*1.1; + height: $baseline*1.1; z-index: 1; } legend { - margin-bottom: var(--baseline); + margin-bottom: $baseline; max-width: 100%; white-space: normal; } legend + .question-description { - margin-top: calc(-1 * var(--baseline)); + margin-top: -$baseline; max-width: 100%; white-space: normal; } @@ -332,24 +332,24 @@ div.problem { // Summary status indicators shown after the input area div.problem { .indicator-container { - @include margin-left(calc((var(--baseline)*0.75))); + @include margin-left($baseline*0.75); .status { - width: var(--baseline); + width: $baseline; // CASE: correct answer &.correct { - @include status-icon(var(--correct), $checkmark-icon); + @include status-icon($correct, $checkmark-icon); } // CASE: partially correct answer &.partially-correct { - @include status-icon(var(--partially-correct), $asterisk-icon); + @include status-icon($partially-correct, $asterisk-icon); } // CASE: incorrect answer &.incorrect { - @include status-icon(var(--incorrect), $cross-icon); + @include status-icon($incorrect, $cross-icon); } &.submitted, @@ -379,7 +379,7 @@ div.problem { .solution-span { > span { - margin: var(--baseline) 0; + margin: $baseline 0; display: block; position: relative; @@ -413,20 +413,20 @@ div.problem { font-style: normal; &:hover { - color: var(--blue); + color: $blue; } } } &.correct, &.ui-icon-check { input { - border-color: var(--correct); + border-color: $correct; } } &.partially-correct, &.ui-icon-check { input { - border-color: var(--partially-correct); + border-color: $partially-correct; } } @@ -438,25 +438,25 @@ div.problem { &.ui-icon-close { input { - border-color: var(--incorrect); + border-color: $incorrect; } } &.incorrect, &.incomplete { input { - border-color: var(--incorrect); + border-color: $incorrect; } } &.submitted, &.ui-icon-check { input { - border-color: var(--submitted); + border-color: $submitted; } } p.answer { display: inline-block; - margin-top: calc((var(--baseline) / 2)); + margin-top: ($baseline / 2); margin-bottom: 0; &::before { @@ -483,7 +483,7 @@ div.problem { } img.loading { - @include padding-left(calc((var(--baseline)/2))); + @include padding-left($baseline/2); display: inline-block; } @@ -517,7 +517,7 @@ div.problem { top: 4px; width: 14px; height: 14px; - background: var(--icon-unanswered) center center no-repeat; + background: url('#{$static-path}/images/unanswered-icon.png') center center no-repeat; } &.processing, &.ui-icon-processing { @@ -526,7 +526,7 @@ div.problem { top: 6px; width: 25px; height: 20px; - background: var(--icon-spinner) center center no-repeat; + background: url('#{$static-path}/images/spinner.gif') center center no-repeat; } &.ui-icon-check { @@ -535,7 +535,7 @@ div.problem { top: 3px; width: 25px; height: 20px; - background: var(--icon-correct) center center no-repeat; + background: url('#{$static-path}/images/correct-icon.png') center center no-repeat; } &.incomplete, &.ui-icon-close { @@ -544,24 +544,24 @@ div.problem { top: 3px; width: 20px; height: 20px; - background: var(--icon-incorrect) center center no-repeat; + background: url('#{$static-path}/images/incorrect-icon.png') center center no-repeat; } } .reload { @include float(right); - margin: calc((var(--baseline)/2)); + margin: ($baseline/2); } .grader-status { @include clearfix(); - margin: calc(var(--baseline)/2) 0; - padding: calc(var(--baseline)/2); + margin: $baseline/2 0; + padding: $baseline/2; border-radius: 5px; - background: var(--gray-l6); + background: $gray-l6; span { display: block; @@ -574,7 +574,7 @@ div.problem { .grading { margin: 0px 7px 0 0; padding-left: 25px; - background: var(--icon-info) left center no-repeat; + background: url('#{$static-path}/images/info-icon.png') left center no-repeat; text-indent: 0px; } @@ -586,11 +586,11 @@ div.problem { } &.file { - margin-top: var(--baseline); - padding: var(--baseline) 0 0 0; + margin-top: $baseline; + padding: $baseline 0 0 0; border: 0; border-top: 1px solid #eee; - background: var(--white); + background: $white; p.debug { display: none; @@ -605,13 +605,13 @@ div.problem { .evaluation { p { - margin-bottom: calc((var(--baseline)/5)); + margin-bottom: ($baseline/5); } } .feedback-on-feedback { - margin-right: var(--baseline); + margin-right: $baseline; height: 100px; } @@ -646,7 +646,7 @@ div.problem { } .submit-message-container { - margin: var(--baseline) 0px ; + margin: $baseline 0px ; } } @@ -753,17 +753,17 @@ div.problem { padding: 0px 5px; border: 1px solid #eaeaea; border-radius: 3px; - background-color: var(--gray-l6); + background-color: $gray-l6; white-space: nowrap; font-size: .9em; } pre { overflow: auto; - padding: 6px calc(var(--baseline)/2); - border: 1px solid var(--gray-l3); + padding: 6px $baseline/2; + border: 1px solid $gray-l3; border-radius: 3px; - background-color: var(--gray-l6); + background-color: $gray-l6; font-size: .9em; line-height: 1.4; @@ -784,7 +784,7 @@ div.problem { input { box-sizing: border-box; - border: 2px solid var(--gray-l4); + border: 2px solid $gray-l4; border-radius: 3px; min-width: 160px; height: 46px; @@ -792,47 +792,47 @@ div.problem { .status { display: inline-block; - margin-top: calc((var(--baseline)/2)); + margin-top: ($baseline/2); background: none; } // CASE: incorrect answer > .incorrect { input { - border: 2px solid var(--incorrect); + border: 2px solid $incorrect; } .status { - @include status-icon(var(--incorrect), $cross-icon); + @include status-icon($incorrect, $cross-icon); } } // CASE: partially correct answer > .partially-correct { input { - border: 2px solid var(--partially-correct); + border: 2px solid $partially-correct; } .status { - @include status-icon(var(--partially-correct), $asterisk-icon); + @include status-icon($partially-correct, $asterisk-icon); } } // CASE: correct answer > .correct { input { - border: 2px solid var(--correct); + border: 2px solid $correct; } .status { - @include status-icon(var(--correct), $checkmark-icon); + @include status-icon($correct, $checkmark-icon); } } // CASE: submitted, correctness withheld > .submitted { input { - border: 2px solid var(--submitted); + border: 2px solid $submitted; } .status { @@ -843,7 +843,7 @@ div.problem { // CASE: unanswered and unsubmitted > .unanswered, > .unsubmitted { input { - border: 2px solid var(--gray-l4); + border: 2px solid $gray-l4; } .status { @@ -868,7 +868,7 @@ div.problem { } .trailing_text { - @include margin-right(calc((var(--baseline)/2))); + @include margin-right($baseline/2); display: inline-block; } @@ -930,7 +930,7 @@ div.problem { visibility: hidden; width: 0; border-right: none; - border-left: 1px solid var(--black); + border-left: 1px solid $black; } } } @@ -952,14 +952,14 @@ div.problem { .capa-message { display: inline-block; - color: var(--gray-d1); + color: $gray-d1; -webkit-font-smoothing: antialiased; } // +Problem - Actions // ==================== div.problem .action { - min-height: var(--baseline); + min-height: $baseline; width: 100%; display: flex; display: -ms-flexbox; @@ -972,11 +972,11 @@ div.problem .action { display: inline-flex; justify-content: flex-end; width: 100%; - padding-bottom: var(--baseline); + padding-bottom: $baseline; } .problem-action-button-wrapper { - @include border-right(1px solid var(--gray-300)); + @include border-right(1px solid $gray-300); @include padding(0, 13px); // to create a 26px gap, which is an a11y recommendation display: inline-block; @@ -994,11 +994,11 @@ div.problem .action { &:hover, &:focus, &:active { - color: var(--primary) !important; + color: $primary !important; } .icon { - margin-bottom: calc(var(--baseline) / 10); + margin-bottom: $baseline / 10; display: block; } @@ -1008,41 +1008,41 @@ div.problem .action { } .submit-attempt-container { - padding-bottom: var(--baseline); + padding-bottom: $baseline; flex-grow: 1; display: flex; align-items: center; - @media (max-width: var(--bp-screen-lg)) { + @media (max-width: $bp-screen-lg) { max-width: 100%; - padding-bottom: var(--baseline); + padding-bottom: $baseline; } .submit { - @include margin-right(calc((var(--baseline) / 2))); + @include margin-right($baseline / 2); @include float(left); white-space: nowrap; } .submit-cta-description { - color: var(--primary); + color: $primary; font-size: small; - padding-right: calc(var(--baseline) / 2); + padding-right: $baseline / 2; } .submit-cta-link-button { - color: var(--primary); - padding-right: calc(var(--baseline) / 4); + color: $primary; + padding-right: $baseline / 4; } } .submission-feedback { - @include margin-right(calc((var(--baseline) / 2))); + @include margin-right($baseline / 2); - margin-top: calc(var(--baseline) / 2); + margin-top: $baseline / 2; display: inline-block; - color: var(--gray-d1); - font-size: var(--medium-font-size); + color: $gray-d1; + font-size: $medium-font-size; -webkit-font-smoothing: antialiased; vertical-align: middle; @@ -1082,7 +1082,7 @@ div.problem { display: block; margin: lh() 0; padding: lh(); - border: 1px solid var(--gray-l3); + border: 1px solid $gray-l3; } .message { @@ -1114,52 +1114,52 @@ div.problem { } div.capa_alert { - margin-top: var(--baseline); + margin-top: $baseline; padding: 8px 12px; - border: 1px solid var(--warning-color); + border: 1px solid $warning-color; border-radius: 3px; - background: var(--warning-color-accent); + background: $warning-color-accent; font-size: 0.9em; } .notification { @include float(left); - margin-top: calc(var(--baseline) / 2); - padding: calc((var(--baseline) / 2.5)) calc((var(--baseline) / 2)) calc((var(--baseline) / 5)) calc((var(--baseline) / 2)); - line-height: var(--base-line-height); + margin-top: $baseline / 2; + padding: ($baseline / 2.5) ($baseline / 2) ($baseline / 5) ($baseline / 2); + line-height: $base-line-height; &.success { - @include notification-by-type(var(--success)); + @include notification-by-type($success); } &.error { - @include notification-by-type(var(--danger)); + @include notification-by-type($danger); } &.warning { - @include notification-by-type(var(--warning)); + @include notification-by-type($warning); } &.general { - @include notification-by-type(var(--general-color-accent)); + @include notification-by-type($general-color-accent); } &.problem-hint { - border: 1px solid var(--uxpl-gray-background); + border: 1px solid $uxpl-gray-background; border-radius: 6px; .icon { - @include margin-right(calc(3 * var(--baseline) / 4) ); + @include margin-right(3 * $baseline / 4); - color: var(--uxpl-gray-dark); + color: $uxpl-gray-dark; } li { - color: var(--uxpl-gray-base); + color: $uxpl-gray-base; strong { - color: var(--uxpl-gray-dark); + color: $uxpl-gray-dark; } } } @@ -1168,7 +1168,7 @@ div.problem { @include float(left); position: relative; - top: calc(var(--baseline) / 5); + top: $baseline / 5; } .notification-message { @@ -1184,7 +1184,7 @@ div.problem { margin: 0; li:not(:last-child) { - margin-bottom: calc(var(--baseline) / 4); + margin-bottom: $baseline / 4; } } } @@ -1198,13 +1198,13 @@ div.problem { .notification-btn { @include float(right); - padding: calc((var(--baseline) / 10)) calc((var(--baseline) / 4)); - min-width: calc((var(--baseline) * 3)); + padding: ($baseline / 10) ($baseline / 4); + min-width: ($baseline * 3); display: block; clear: both; &:first-child { - margin-bottom: calc(var(--baseline) / 4); + margin-bottom: $baseline / 4; } } @@ -1225,26 +1225,26 @@ div.problem { &.btn-brand { &:hover { - background-color: var(--btn-brand-focus-background); + background-color: $btn-brand-focus-background; } } } .review-btn { - color: var(--blue); // notification type has other colors + color: $blue; // notification type has other colors &.sr { - color: var(--blue); + color: $blue; } } div.capa_reset { padding: 25px; - background-color: var(--error-color-light); - border: 1px solid var(--error-color); + border: 1px solid $error-color; + background-color: lighten($error-color, 25%); border-radius: 3px; font-size: 1em; - margin-top: calc(var(--baseline)/2); - margin-bottom: calc(var(--baseline)/2); + margin-top: $baseline/2; + margin-bottom: $baseline/2; } .capa_reset>h2 { @@ -1256,7 +1256,7 @@ div.problem { } .hints { - border: 1px solid var(--gray-l3); + border: 1px solid $gray-l3; h3 { @extend %t-strong; @@ -1264,7 +1264,7 @@ div.problem { padding: 9px; border-bottom: 1px solid #e3e3e3; background: #eee; - text-shadow: 0 1px 0 var(--white); + text-shadow: 0 1px 0 $white; font-size: em(16); } @@ -1283,8 +1283,8 @@ div.problem { a { display: block; padding: 9px; - background: var(--gray-l6); - box-shadow: inset 0 0 0 1px var(--white); + background: $gray-l6; + box-shadow: inset 0 0 0 1px $white; } } @@ -1311,11 +1311,11 @@ div.problem { > section { position: relative; - margin-bottom: calc((var(--baseline)/2)); - padding: 9px 9px var(--baseline); + margin-bottom: ($baseline/2); + padding: 9px 9px $baseline; border: 1px solid #ddd; border-radius: 3px; - background: var(--white); + background: $white; box-shadow: inset 0 0 0 1px #eee; p:last-of-type { @@ -1331,8 +1331,8 @@ div.problem { box-sizing: border-box; display: block; - padding: calc((var(--baseline)/5)); - background: var(--gray-l4); + padding: ($baseline/5); + background: $gray-l4; text-align: right; font-size: 1em; @@ -1349,8 +1349,8 @@ div.problem { .external-grader-message { section { - padding-top: calc((var(--baseline)*1.5)); - padding-left: var(--baseline); + padding-top: ($baseline*1.5); + padding-left: $baseline; background-color: #fafafa; color: #2c2c2c; font-size: 1em; @@ -1369,9 +1369,9 @@ div.problem { padding: 0; .result-errors { - margin: calc((var(--baseline)/4)); - padding: calc((var(--baseline)/2)) calc((var(--baseline)/2)) calc((var(--baseline)/2)) calc((var(--baseline)*2)); - background: var(--icon-incorrect) center left no-repeat; + margin: ($baseline/4); + padding: ($baseline/2) ($baseline/2) ($baseline/2) ($baseline*2); + background: url('#{$static-path}/images/incorrect-icon.png') center left no-repeat; li { color: #b00; @@ -1379,10 +1379,10 @@ div.problem { } .result-output { - margin: calc(var(--baseline)/4); - padding: var(--baseline) 0 calc((var(--baseline)*0.75)) 50px; + margin: $baseline/4; + padding: $baseline 0 ($baseline*0.75) 50px; border-top: 1px solid #ddd; - border-left: var(--baseline) solid #fafafa; + border-left: $baseline solid #fafafa; h4 { font-size: 1em; @@ -1394,7 +1394,7 @@ div.problem { } dt { - margin-top: var(--baseline); + margin-top: $baseline; } dd { @@ -1403,7 +1403,7 @@ div.problem { } .result-correct { - background: var(--icon-correct) left 20px no-repeat; + background: url('#{$static-path}/images/correct-icon.png') left 20px no-repeat; .result-actual-output { color: #090; @@ -1411,7 +1411,7 @@ div.problem { } .result-partially-correct { - background: var(--icon-partially-correct) left 20px no-repeat; + background: url('#{$static-path}/images/partially-correct-icon.png') left 20px no-repeat; .result-actual-output { color: #090; @@ -1419,7 +1419,7 @@ div.problem { } .result-incorrect { - background: var(--icon-incorrect) left 20px no-repeat; + background: url('#{$static-path}/images/incorrect-icon.png') left 20px no-repeat; .result-actual-output { color: #b00; @@ -1427,8 +1427,8 @@ div.problem { } .markup-text{ - margin: calc((var(--baseline)/4)); - padding: var(--baseline) 0 15px 50px; + margin: ($baseline/4); + padding: $baseline 0 15px 50px; border-top: 1px solid #ddd; border-left: 20px solid #fafafa; @@ -1451,19 +1451,19 @@ div.problem { div.problem { .rubric { tr { - margin: calc((var(--baseline)/2)) 0; + margin: ($baseline/2) 0; height: 100%; } td { - margin: calc((var(--baseline)/2)) 0; - padding: var(--baseline) 0; + margin: ($baseline/2) 0; + padding: $baseline 0; height: 100%; } th { - margin: calc((var(--baseline)/4)); - padding: calc((var(--baseline)/4)); + margin: ($baseline/4); + padding: ($baseline/4); } label, @@ -1471,12 +1471,12 @@ div.problem { position: relative; display: inline-block; margin: 3px; - padding: calc((var(--baseline)*0.75)); + padding: ($baseline*0.75); min-width: 50px; min-height: 50px; width: 150px; height: 100%; - background-color: var(--gray-l3); + background-color: $gray-l3; font-size: .9em; } @@ -1484,7 +1484,7 @@ div.problem { position: absolute; right: 0; bottom: 0; - margin: calc((var(--baseline)/2)); + margin: ($baseline/2); } .selected-grade { @@ -1508,14 +1508,14 @@ div.problem { div.problem { .annotation-input { margin: 0 0 1em 0; - border: 1px solid var(--gray-l3); + border: 1px solid $gray-l3; border-radius: 1em; .annotation-header { @extend %t-strong; padding: .5em 1em; - border-bottom: 1px solid var(--gray-l3); + border-bottom: 1px solid $gray-l3; } .annotation-body { padding: .5em 1em; } @@ -1557,7 +1557,7 @@ div.problem { @extend %ui-fake-link; display: inline-block; - margin-left: calc((var(--baseline)*2)); + margin-left: ($baseline*2); border: 1px solid rgb(102,102,102); &.selected { @@ -1590,13 +1590,13 @@ div.problem { .debug-value { margin: 1em 0; padding: 1em; - border: 1px solid var(--black); + border: 1px solid $black; background-color: #999; - color: var(--white); + color: $white; input[type="text"] { width: 100%; } - pre { background-color: var(--gray-l3); color: var(--black); } + pre { background-color: $gray-l3; color: $black; } &::before { @extend %t-strong; @@ -1623,7 +1623,7 @@ div.problem { @extend label.choicegroup_correct; input[type="text"] { - border-color: var(--correct); + border-color: $correct; } } @@ -1631,7 +1631,7 @@ div.problem { @extend label.choicegroup_partially-correct; input[type="text"] { - border-color: var(--partially-correct); + border-color: $partially-correct; } } @@ -1645,9 +1645,9 @@ div.problem { label.choicetextgroup_show_correct, section.choicetextgroup_show_correct { &::after { - @include margin-left(calc((var(--baseline)*0.75))); + @include margin-left($baseline*0.75); - content: var(--icon-correct); + content: url('#{$static-path}/images/correct-icon.png'); } } @@ -1682,15 +1682,15 @@ div.problem .imageinput.capa_inputtype { } .correct { - @include status-icon(var(--correct), $checkmark-icon); + @include status-icon($correct, $checkmark-icon); } .incorrect { - @include status-icon(var(--incorrect), $cross-icon); + @include status-icon($incorrect, $cross-icon); } .partially-correct { - @include status-icon(var(--partially-correct), $asterisk-icon); + @include status-icon($partially-correct, $asterisk-icon); } .submitted { @@ -1723,15 +1723,15 @@ div.problem .annotation-input { } .correct { - @include status-icon(var(--correct), $checkmark-icon); + @include status-icon($correct, $checkmark-icon); } .incorrect { - @include status-icon(var(--incorrect), $cross-icon); + @include status-icon($incorrect, $cross-icon); } .partially-correct { - @include status-icon(var(--partially-correct), $asterisk-icon); + @include status-icon($partially-correct, $asterisk-icon); } .submitted { @@ -1743,5 +1743,5 @@ div.problem .annotation-input { // ==================== .problems-wrapper .loading-spinner { text-align: center; - color: var(--gray-d1); + color: $gray-d1; } diff --git a/xmodule/assets/editor/_edit.scss b/xmodule/assets/editor/_edit.scss index 9ecd31416ced..71699776522d 100644 --- a/xmodule/assets/editor/_edit.scss +++ b/xmodule/assets/editor/_edit.scss @@ -18,7 +18,7 @@ @include linear-gradient(top, #d4dee8, #c9d5e2); position: relative; - padding: calc((var(--baseline)/4)); + padding: ($baseline/4); border-bottom-color: #a5aaaf; button { @@ -26,7 +26,7 @@ @include float(left); - padding: 3px calc((var(--baseline)/2)) 5px; + padding: 3px ($baseline/2) 5px; margin-left: 7px; border: 0; border-radius: 2px; @@ -53,7 +53,7 @@ li { @include float(left); - @include margin-right(calc((var(--baseline)/4))); + @include margin-right($baseline/4); &:last-child { @include margin-right(0); @@ -67,7 +67,7 @@ border: 1px solid #a5aaaf; border-radius: 3px 3px 0 0; - @include linear-gradient(top, var(--transparent) 87%, rgba(0, 0, 0, .06)); + @include linear-gradient(top, $transparent 87%, rgba(0, 0, 0, .06)); background-color: #e5ecf3; font-size: 13px; @@ -75,8 +75,8 @@ box-shadow: 1px -1px 1px rgba(0, 0, 0, .05); &.current { - background: var(--white); - border-bottom-color: var(--white); + background: $white; + border-bottom-color: $white; } } } diff --git a/xmodule/assets/html/_display.scss b/xmodule/assets/html/_display.scss index beceaa1d0119..25e2ce4fbd64 100644 --- a/xmodule/assets/html/_display.scss +++ b/xmodule/assets/html/_display.scss @@ -10,8 +10,8 @@ } h1 { - color: var(--body-color); - font: normal 2em/1.4em var(--font-family-sans-serif); + color: $body-color; + font: normal 2em/1.4em $font-family-sans-serif; letter-spacing: 1px; @include margin(0, 0, 1.416em, 0); @@ -19,9 +19,9 @@ h1 { h2 { color: #646464; - font: normal 1.2em/1.2em var(--font-family-sans-serif); + font: normal 1.2em/1.2em $font-family-sans-serif; letter-spacing: 1px; - margin-bottom: calc((var(--baseline)*0.75)); + margin-bottom: ($baseline*0.75); -webkit-font-smoothing: antialiased; } @@ -29,7 +29,7 @@ h3, h4, h5, h6 { - @include margin(0, 0, calc((var(--baseline)/2)), 0); + @include margin(0, 0, ($baseline/2), 0); font-weight: 600; } @@ -54,7 +54,7 @@ p { margin-bottom: 1.416em; font-size: 1em; line-height: 1.6em !important; - color: var(--body-color); + color: $body-color; } em, @@ -78,11 +78,11 @@ b { p + p, ul + p, ol + p { - margin-top: var(--baseline); + margin-top: $baseline; } blockquote { - margin: 1em calc((var(--baseline)*2)); + margin: 1em ($baseline*2); } ol, @@ -91,7 +91,7 @@ ul { @include bi-app-compact(padding, 0, 0, 0, 1em); margin: 1em 0; - color: var(--body-color); + color: $body-color; li { margin-bottom: 0.708em; @@ -112,7 +112,7 @@ a { &:hover, &:active, &:focus { - color: var(--blue); + color: $blue; } } @@ -122,7 +122,7 @@ img { pre { margin: 1em 0; - color: var(--body-color); + color: $body-color; font-family: monospace, serif; font-size: 1em; white-space: pre-wrap; @@ -130,7 +130,7 @@ pre { } code { - color: var(--body-color); + color: $body-color; font-family: monospace, serif; background: none; padding: 0; @@ -138,15 +138,15 @@ code { table { width: 100%; - margin: var(--baseline) 0; + margin: $baseline 0; border-collapse: collapse; font-size: 16px; td, th { - margin: var(--baseline) 0; - padding: calc((var(--baseline)/2)); - border: 1px solid var(--gray-l3); + margin: $baseline 0; + padding: ($baseline/2); + border: 1px solid $gray-l3; font-size: 14px; &.cont-justified-left { @@ -179,12 +179,12 @@ th { position: absolute; display: block; - padding: calc((var(--baseline)/4)) 7px; + padding: ($baseline/4) 7px; border-radius: 5px; opacity: 0.9; - background: var(--white); - color: var(--black); - border: 2px solid var(--black); + background: $white; + color: $black; + border: 2px solid $black; .label { font-weight: bold; @@ -269,11 +269,11 @@ th { position: relative; &.action-zoom-in { - margin-right: calc((var(--baseline)/4)); + margin-right: ($baseline/4); } &.action-zoom-out { - margin-left: calc((var(--baseline)/4)); + margin-left: ($baseline/4); } &.is-disabled { diff --git a/xmodule/assets/lti/_lti.scss b/xmodule/assets/lti/_lti.scss index 9eee710f0dad..4bd2c41317f5 100644 --- a/xmodule/assets/lti/_lti.scss +++ b/xmodule/assets/lti/_lti.scss @@ -10,7 +10,7 @@ h2.problem-header { div.problem-progress { display: inline-block; - padding-left: calc((var(--baseline)/4)); + padding-left: ($baseline/4); color: #666; font-weight: 100; font-size: em(16); @@ -24,8 +24,8 @@ div.lti { .wrapper-lti-link { @include font-size(14); - background-color: var(--sidebar-color); - padding: var(--baseline); + background-color: $sidebar-color; + padding: $baseline; .lti-link { margin-bottom: 0; @@ -58,8 +58,8 @@ div.lti { } div.problem-feedback { - margin-top: calc((var(--baseline)/4)); - margin-bottom: calc((var(--baseline)/4)); + margin-top: ($baseline/4); + margin-bottom: ($baseline/4); } } diff --git a/xmodule/assets/poll/_display.scss b/xmodule/assets/poll/_display.scss index 7c9b21bf205e..7c07f89376b2 100644 --- a/xmodule/assets/poll/_display.scss +++ b/xmodule/assets/poll/_display.scss @@ -20,13 +20,13 @@ div.poll_question { h3 { margin-top: 0; - margin-bottom: calc((var(--baseline)*0.75)); + margin-bottom: ($baseline*0.75); color: #fe57a1; font-size: 1.9em; &.problem-header { div.staff { - margin-top: calc((var(--baseline)*1.5)); + margin-top: ($baseline*1.5); font-size: 80%; } } @@ -44,7 +44,7 @@ div.poll_question { } .poll_answer { - margin-bottom: var(--baseline); + margin-bottom: $baseline; &.short { clear: both; @@ -107,7 +107,7 @@ div.poll_question { font-weight: bold; letter-spacing: normal; line-height: 25.59375px; - margin-bottom: calc((var(--baseline)*0.75)); + margin-bottom: ($baseline*0.75); margin: 0; padding: 0px; text-align: center; @@ -145,9 +145,9 @@ div.poll_question { width: 80%; text-align: left; min-height: 30px; - margin-left: var(--baseline); + margin-left: $baseline; height: auto; - margin-bottom: var(--baseline); + margin-bottom: $baseline; &.short { width: 100px; @@ -157,7 +157,7 @@ div.poll_question { .stats { min-height: 40px; - margin-top: var(--baseline); + margin-top: $baseline; clear: both; &.short { @@ -174,7 +174,7 @@ div.poll_question { border: 1px solid black; display: inline; float: left; - margin-right: calc((var(--baseline)/2)); + margin-right: ($baseline/2); &.short { width: 65%; diff --git a/xmodule/assets/problem/_edit.scss b/xmodule/assets/problem/_edit.scss index f3fc795ec646..018a0961c247 100644 --- a/xmodule/assets/problem/_edit.scss +++ b/xmodule/assets/problem/_edit.scss @@ -5,20 +5,20 @@ margin-top: -4px; padding: 3px 9px; font-size: 12px; - color: var(--link-color); + color: $link-color; &.current { - border: 1px solid var(--lightGrey) !important; + border: 1px solid $lightGrey !important; border-radius: 3px !important; - background: var(--lightGrey) !important; - color: var(--darkGrey) !important; + background: $lightGrey !important; + color: $darkGrey !important; pointer-events: none; cursor: none; &:hover, &:focus { box-shadow: 0 0 0 0 !important; - background-color: var(--white); + background-color: $white; } } } @@ -31,9 +31,9 @@ top: 41px; @include left(70%); width: 0; - border-left: 1px solid var(--gray-l2); + border-left: 1px solid $gray-l2; - background-color: var(--lightGrey); + background-color: $lightGrey; overflow: hidden; &.shown { @@ -76,7 +76,7 @@ margin-right: 30px; .icon { - height: calc((var(--baseline) * 1.5)); + height: ($baseline * 1.5); } } } @@ -105,5 +105,5 @@ width: 26px; height: 21px; vertical-align: middle; - color: var(--body-color); + color: $body-color; } diff --git a/xmodule/assets/sequence/_display.scss b/xmodule/assets/sequence/_display.scss index 595b602a8872..3ddda8b37d09 100644 --- a/xmodule/assets/sequence/_display.scss +++ b/xmodule/assets/sequence/_display.scss @@ -5,9 +5,9 @@ @import 'bootstrap/scss/mixins/breakpoints'; @import 'lms/theme/variables-v1'; -$seq-nav-border-color: var(--border-color) !default; +$seq-nav-border-color: $border-color !default; $seq-nav-hover-color: rgb(245, 245, 245) !default; -$seq-nav-link-color: var(--link-color) !default; +$seq-nav-link-color: $link-color !default; $seq-nav-icon-color: rgb(10, 10, 10) !default; $seq-nav-icon-color-muted: rgb(90, 90, 90) !default; $seq-nav-tooltip-color: rgb(51, 51, 51) !default; @@ -69,7 +69,7 @@ $seq-nav-height: 50px; .sequence-nav { @extend .topbar; - margin: 0 auto var(--baseline); + margin: 0 auto $baseline; position: relative; border-bottom: none; z-index: 0; @@ -172,14 +172,14 @@ $seq-nav-height: 50px; margin-top: 12px; background: $seq-nav-tooltip-color; - color: var(--white); - font-family: var(--font-family-sans-serif); + color: $white; + font-family: $font-family-sans-serif; line-height: lh(); right: 0; // Should not be RTLed, tooltips do not move in RTL padding: 6px; position: absolute; top: 48px; - text-shadow: 0 -1px 0 var(--black); + text-shadow: 0 -1px 0 $black; white-space: pre; pointer-events: none; @@ -239,7 +239,7 @@ $seq-nav-height: 50px; text-overflow: ellipsis; span:not(:last-child) { - @include padding-right(calc((var(--baseline) / 2))); + @include padding-right($baseline / 2); } } diff --git a/xmodule/assets/tabs/_codemirror.scss b/xmodule/assets/tabs/_codemirror.scss index 37a894a10395..237d1850332a 100644 --- a/xmodule/assets/tabs/_codemirror.scss +++ b/xmodule/assets/tabs/_codemirror.scss @@ -9,7 +9,7 @@ height: 379px; border: 1px solid #3c3c3c; border-top: 1px solid #8891a1; - background: var(--white); + background: $white; color: #3c3c3c; } diff --git a/xmodule/assets/tabs/_tabs.scss b/xmodule/assets/tabs/_tabs.scss index 4b8c2a387a91..ad47d915a230 100644 --- a/xmodule/assets/tabs/_tabs.scss +++ b/xmodule/assets/tabs/_tabs.scss @@ -31,12 +31,12 @@ .edit-header { box-sizing: border-box; - padding: 18px var(--baseline); + padding: 18px $baseline; top: 0 !important; // ugly override for second level tab override right: 0; - background-color: var(--blue); - border-bottom: 1px solid var(--blue-d2); - color: var(--white); + background-color: $blue; + border-bottom: 1px solid $blue-d2; + color: $white; //Component Name .component-name { @@ -44,16 +44,16 @@ top: 0; left: 0; width: 50%; - color: var(--white); + color: $white; font-weight: 600; em { display: inline-block; - margin-right: calc((var(--baseline)/4)); + margin-right: ($baseline/4); font-weight: 400; - color: var(--white); + color: $white; } } @@ -61,9 +61,9 @@ .editor-tabs { list-style: none; right: 0; - top: calc((var(--baseline)/4)); + top: ($baseline/4); position: absolute; - padding: 12px calc((var(--baseline)*0.75)); + padding: 12px ($baseline*0.75); .inner_tab_wrap { display: inline-block; @@ -73,25 +73,25 @@ @include font-size(14); @include linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0)); - border: 1px solid var(--blue-d1); + border: 1px solid $blue-d1; border-radius: 3px; - padding: calc((var(--baseline)/4)) (var(--baseline)); - background-color: var(--blue); + padding: ($baseline/4) ($baseline); + background-color: $blue; font-weight: bold; - color: var(--white); + color: $white; &.current { - @include linear-gradient(var(--blue), var(--blue)); + @include linear-gradient($blue, $blue); - color: var(--blue-d1); - box-shadow: inset 0 1px 2px 1px var(--shadow-l1); - background-color: var(--blue-d4); + color: $blue-d1; + box-shadow: inset 0 1px 2px 1px $shadow-l1; + background-color: $blue-d4; cursor: default; } &:hover, &:focus { - box-shadow: inset 0 1px 2px 1px var(--shadow); + box-shadow: inset 0 1px 2px 1px $shadow; background-image: linear-gradient(#009fe6, #009fe6) !important; } } @@ -113,7 +113,7 @@ .comp-subtitles-import-list { > li { display: block; - margin: calc(var(--baseline)/2) 0; + margin: $baseline/2 0; } .blue-button { @@ -128,7 +128,7 @@ } .component-tab { - background: var(--white); + background: $white; position: relative; border-top: 1px solid #8891a1; diff --git a/xmodule/assets/video/_accessible_menu.scss b/xmodule/assets/video/_accessible_menu.scss index d411925f2390..f7153fa98429 100644 --- a/xmodule/assets/video/_accessible_menu.scss +++ b/xmodule/assets/video/_accessible_menu.scss @@ -1,12 +1,11 @@ @import 'base/mixins'; -@import 'lms/theme/variables-v1'; $a11y--gray: rgb(127, 127, 127); $a11y--blue: rgb(0, 159, 230); -$a11y--gray-d1: var(--gray-d1); -$a11y--gray-l2: var(--gray-l2); -$a11y--gray-l3: var(--gray-l3); -$a11y--blue-s1: var(--blue-s1); +$a11y--gray-d1: shade($gray, 20%); +$a11y--gray-l2: tint($gray, 40%); +$a11y--gray-l3: tint($gray, 60%); +$a11y--blue-s1: saturate($blue, 15%); %use-font-awesome { font-family: FontAwesome; @@ -33,7 +32,7 @@ $a11y--blue-s1: var(--blue-s1); display: none; position: absolute; list-style: none; - background-color: var(--white); + background-color: $white; border: 1px solid #eee; li { @@ -42,7 +41,7 @@ $a11y--blue-s1: var(--blue-s1); margin: 0; padding: 0; border-bottom: 1px solid #eee; - color: var(--white); + color: $white; a { display: block; @@ -85,23 +84,23 @@ $a11y--blue-s1: var(--blue-s1); &.open { > a { - background-color: var(--action-primary-active-bg); - color: var(--very-light-text); + background-color: $action-primary-active-bg; + color: $very-light-text; &::after { - color: var(--very-light-text); + color: $very-light-text; } } } > a { - @include transition(all var(--tmg-f2) ease-in-out 0s); + @include transition(all $tmg-f2 ease-in-out 0s); @include font-size(12); display: block; border-radius: 0 3px 3px 0; - background-color: var(--very-light-text); - padding: calc((var(--baseline)*0.75)) calc((var(--baseline)*1.25)) calc((var(--baseline)*0.75)) calc((var(--baseline)*0.75)); + background-color: $very-light-text; + padding: ($baseline*0.75) ($baseline*1.25) ($baseline*0.75) ($baseline*0.75); color: $a11y--gray-l2; min-width: 1.5em; line-height: 14px; @@ -114,9 +113,9 @@ $a11y--blue-s1: var(--blue-s1); content: "\f0d7"; position: absolute; - right: calc((var(--baseline)*0.5)); + right: ($baseline*0.5); top: 33%; - color: var(--lighter-base-font-color); + color: $lighter-base-font-color; } } @@ -145,7 +144,7 @@ $a11y--blue-s1: var(--blue-s1); @extend %ui-depth5; border: 1px solid #333; - background: var(--white); + background: $white; color: #333; padding: 0; margin: 0; @@ -163,8 +162,8 @@ $a11y--blue-s1: var(--blue-s1); .menu-item, .submenu-item { - border-top: 1px solid var(--gray-l3); - padding: calc((var(--baseline)/4)) calc((var(--baseline)/2)); + border-top: 1px solid $gray-l3; + padding: ($baseline/4) ($baseline/2); outline: none; & > span { @@ -177,17 +176,17 @@ $a11y--blue-s1: var(--blue-s1); &:focus { background: #333; - color: var(--white); + color: $white; & > span { - color: var(--white); + color: $white; } } } .submenu-item { position: relative; - padding: calc((var(--baseline)/4)) var(--baseline) calc((var(--baseline)/4)) calc((var(--baseline)/2)); + padding: ($baseline/4) $baseline ($baseline/4) ($baseline/2); &::after { content: '\25B6'; @@ -203,10 +202,10 @@ $a11y--blue-s1: var(--blue-s1); &.is-opened { background: #333; - color: var(--white); + color: $white; & > span { - color: var(--white); + color: $white; } & > .submenu { @@ -221,7 +220,7 @@ $a11y--blue-s1: var(--blue-s1); .is-disabled { pointer-events: none; - color: var(--gray-l3); + color: $gray-l3; } } diff --git a/xmodule/assets/video/_display.scss b/xmodule/assets/video/_display.scss index c1f2ccee19fa..fd5cd73b2105 100644 --- a/xmodule/assets/video/_display.scss +++ b/xmodule/assets/video/_display.scss @@ -23,7 +23,7 @@ $secondary-light: rgb(219, 139, 175); // UXPL secondary light $cool-dark: rgb(79, 89, 93); // UXPL cool dark & { - margin-bottom: calc((var(--baseline)*1.5)); + margin-bottom: ($baseline*1.5); } .is-hidden { @@ -99,9 +99,9 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark .branding, .wrapper-transcript-feedback { flex: 1; - margin-top: var(--baseline); + margin-top: $baseline; - @include padding-right(var(--baseline)); + @include padding-right($baseline); vertical-align: top; } @@ -147,14 +147,14 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark left: -9999em; display: inline-block; vertical-align: middle; - color: var(--body-color); + color: $body-color; } .brand-logo { display: inline-block; max-width: 100%; - max-height: calc((var(--baseline)*2)); - padding: calc((var(--baseline)/4)) 0; + max-height: ($baseline*2); + padding: ($baseline/4) 0; vertical-align: middle; } } @@ -180,8 +180,8 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark .google-disclaimer { display: none; - margin-top: var(--baseline); - @include padding-right(var(--baseline)); + margin-top: $baseline; + @include padding-right($baseline); vertical-align: top; } @@ -246,7 +246,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark opacity: 0.1; &::after { - background: var(--white); + background: $white; position: absolute; width: 50%; height: 50%; @@ -271,23 +271,23 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark } .closed-captions.is-visible { - max-height: calc((var(--baseline) * 3)); - border-radius: calc((var(--baseline) / 5)); - padding: 8px calc((var(--baseline) / 2)) 8px calc((var(--baseline) * 1.5)); + max-height: ($baseline * 3); + border-radius: ($baseline / 5); + padding: 8px ($baseline / 2) 8px ($baseline * 1.5); background: rgba(0, 0, 0, 0.75); - color: var(--yellow); + color: $yellow; &::before { position: absolute; display: inline-block; top: 50%; - @include left(var(--baseline)); + @include left($baseline); margin-top: -0.6em; font-family: 'FontAwesome'; content: "\f142"; - color: var(--white); + color: $white; opacity: 0.5; } @@ -316,7 +316,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark .video-error, .video-hls-error { - padding: calc((var(--baseline) / 5)); + padding: ($baseline / 5); background: black; color: white !important; // the pattern library headings shim is more scoped } @@ -366,7 +366,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark margin: 0; border: 0; border-radius: 0; - padding: calc((var(--baseline) / 2)) calc((var(--baseline) / 1.5)); + padding: ($baseline / 2) ($baseline / 1.5); background: rgb(40, 44, 46); // UXPL grayscale-cool x-dark box-shadow: none; text-shadow: none; @@ -409,7 +409,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark left: 0; right: 0; z-index: 1; - height: calc((var(--baseline) / 4)); + height: ($baseline / 4); margin-left: 0; border: 1px solid $cool-dark; border-radius: 0; @@ -436,11 +436,11 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark box-sizing: border-box; top: -1px; - height: calc((var(--baseline) / 4)); - width: calc((var(--baseline) / 4)); - margin-left: calc(-1 * (var(--baseline) / 8)); // center-center causes the control to be beyond the end of the sider + height: ($baseline / 4); + width: ($baseline / 4); + margin-left: -($baseline / 8); // center-center causes the control to be beyond the end of the sider border: 1px solid $secondary-base; - border-radius: calc((var(--baseline) / 5)); + border-radius: ($baseline / 5); padding: 0; background: $secondary-base; box-shadow: none; @@ -527,7 +527,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark position: absolute; display: none; - bottom: calc((var(--baseline) * 2)); + bottom: ($baseline * 2); @include right(0); // right-align menus since this whole collection is on the right @@ -571,9 +571,9 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark &.is-active { .speed-option, .control-lang { - @include border-left(var(--baseline)/10 solid rgb(14, 166, 236)); + @include border-left($baseline/10 solid rgb(14, 166, 236)); - font-weight: var(--font-bold); + font-weight: $font-bold; color: rgb(14, 166, 236); // UXPL primary accent } } @@ -610,9 +610,9 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark .speed-button { .label { - @include padding(0 calc((var(--baseline)/3)) 0 0); + @include padding(0 ($baseline/3) 0 0); - font-family: var(--font-family-sans-serif); + font-family: $font-family-sans-serif; color: rgb(231, 236, 238); // UXPL grayscale-cool x-light @media (max-width: 1120px) { @@ -636,8 +636,8 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark .lang { .language-menu { - width: var(--baseline); - padding: calc((var(--baseline) / 2)) 0; + width: $baseline; + padding: ($baseline / 2) 0; } .control { @@ -685,7 +685,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark display: none; position: absolute; - bottom: calc((var(--baseline) * 2)); + bottom: ($baseline * 2); @include right(0); @@ -695,7 +695,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark .volume-slider { height: 100px; - width: calc((var(--baseline) / 4)); + width: ($baseline / 4); margin: 14px auto; box-sizing: border-box; border: 1px solid $cool-dark; @@ -704,14 +704,14 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark .ui-slider-handle { @extend %ui-fake-link; - @include transition(height var(--tmg-s2) ease-in-out 0s, width var(--tmg-s2) ease-in-out 0s); + @include transition(height $tmg-s2 ease-in-out 0s, width $tmg-s2 ease-in-out 0s); @include left(-5px); box-sizing: border-box; height: 13px; width: 13px; border: 1px solid $secondary-base; - border-radius: calc((var(--baseline) / 5)); + border-radius: ($baseline / 5); padding: 0; background: $secondary-base; box-shadow: none; @@ -763,11 +763,11 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark &:hover { .video-controls { .slider { - height: calc((var(--baseline) / 1.5)); + height: ($baseline / 1.5); .ui-slider-handle { - height: calc((var(--baseline) / 1.5)); - width: calc((var(--baseline) / 1.5)); + height: ($baseline / 1.5); + width: ($baseline / 1.5); } } } @@ -887,7 +887,7 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark bottom: 0; top: 0; width: 275px; - padding: 0 var(--baseline); + padding: 0 $baseline; display: none; } } @@ -973,14 +973,14 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark box-sizing: border-box; @include transition(none); - background: var(--black); + background: $black; visibility: visible; li { color: #aaa; &.current { - color: var(--white); + color: $white; } } } @@ -1010,17 +1010,17 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark background-position: 50% 50%; background-repeat: no-repeat; background-size: 100%; - background-color: var(--black); + background-color: $black; &.is-html5 { background-size: 15%; } .btn-play.btn-pre-roll { - padding: var(--baseline); + padding: $baseline; border: none; - border-radius: var(--baseline); - background: var(--black-t2); + border-radius: $baseline; + background: $black-t2; box-shadow: none; &::after { @@ -1030,13 +1030,13 @@ $cool-dark: rgb(79, 89, 93); // UXPL cool dark } img { - height: calc((var(--baseline) * 4)); - width: calc((var(--baseline) * 4)); + height: ($baseline * 4); + width: ($baseline * 4); } &:hover, &:focus { - background: var(--blue); + background: $blue; } } } From c09286f02efedaeb5608a9e348221405450aca0a Mon Sep 17 00:00:00 2001 From: rijuma <12736783+rijuma@users.noreply.github.com> Date: Thu, 12 Sep 2024 14:38:51 +0000 Subject: [PATCH 5/7] feat: Upgrade Python dependency edx-name-affirmation https://github.com/edx/edx-name-affirmation/pull/215 Commit generated by workflow `openedx/edx-platform/.github/workflows/upgrade-one-python-dependency.yml@refs/heads/master` --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/testing.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index ae29748fa5ae..9c85f60fc3a8 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -482,7 +482,7 @@ edx-i18n-tools==1.5.0 # ora2 edx-milestones==0.6.0 # via -r requirements/edx/kernel.in -edx-name-affirmation==2.3.7 +edx-name-affirmation==2.4.0 # via -r requirements/edx/kernel.in edx-opaque-keys[django]==2.10.0 # via diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index d4946b8f809b..15e078ae0dd3 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -766,7 +766,7 @@ edx-milestones==0.6.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-name-affirmation==2.3.7 +edx-name-affirmation==2.4.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 e3d2197cae86..defc09a23deb 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -562,7 +562,7 @@ edx-i18n-tools==1.5.0 # ora2 edx-milestones==0.6.0 # via -r requirements/edx/base.txt -edx-name-affirmation==2.3.7 +edx-name-affirmation==2.4.0 # via -r requirements/edx/base.txt edx-opaque-keys[django]==2.10.0 # via diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 1407505d6ade..49be07ecec71 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -588,7 +588,7 @@ edx-lint==5.3.7 # via -r requirements/edx/testing.in edx-milestones==0.6.0 # via -r requirements/edx/base.txt -edx-name-affirmation==2.3.7 +edx-name-affirmation==2.4.0 # via -r requirements/edx/base.txt edx-opaque-keys[django]==2.10.0 # via From c41fe8991832de7e987368a533d4b0e9b91fefce Mon Sep 17 00:00:00 2001 From: Jillian Date: Fri, 13 Sep 2024 22:50:54 +0930 Subject: [PATCH 6/7] Store content object collections in search index [FC-0062] (#35469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add Library Collections REST endpoints * test: Add tests for Collections REST APIs * chore: Add missing __init__ files * docs: Add warning about unstable REST APIs * feat: emit CONTENT_OBJECT_ASSOCIATIONS_CHANGED whenever a content object's tags or collections have changed, and handle that event in content/search. The deprecated CONTENT_OBJECT_TAGS_CHANGED event is still emitted when tags change; to be removed after Sumac. * docs: replaces CONTENT_OBJECT_TAGS_CHANGED with CONTENT_OBJECT_ASSOCIATIONS_CHANGED * chore: updates openedx-events==9.14.0 * chore: updates openedx-learning==0.11.4 Co-authored-by: Yusuf Musleh Co-authored-by: Chris Chávez Co-authored-by: Rômulo Penido --- docs/hooks/events.rst | 8 +- openedx/core/djangoapps/content/search/api.py | 35 +++- .../djangoapps/content/search/documents.py | 78 ++++++++- .../djangoapps/content/search/handlers.py | 62 ++++++-- .../core/djangoapps/content/search/tasks.py | 13 ++ .../content/search/tests/test_api.py | 150 ++++++++++++++++-- .../content/search/tests/test_documents.py | 114 ++++++++++++- .../core/djangoapps/content_libraries/api.py | 13 +- .../content_libraries/tests/test_api.py | 24 +-- .../core/djangoapps/content_tagging/api.py | 28 +++- .../content_tagging/rest_api/v1/views.py | 19 ++- 11 files changed, 491 insertions(+), 53 deletions(-) diff --git a/docs/hooks/events.rst b/docs/hooks/events.rst index 2a7561df24e1..bccb98e56a42 100644 --- a/docs/hooks/events.rst +++ b/docs/hooks/events.rst @@ -244,10 +244,6 @@ Content Authoring Events - org.openedx.content_authoring.library_block.deleted.v1 - 2023-07-20 - * - `CONTENT_OBJECT_TAGS_CHANGED `_ - - org.openedx.content_authoring.content.object.tags.changed.v1 - - 2024-03-31 - * - `LIBRARY_COLLECTION_CREATED `_ - org.openedx.content_authoring.content_library.collection.created.v1 - 2024-08-23 @@ -259,3 +255,7 @@ Content Authoring Events * - `LIBRARY_COLLECTION_DELETED `_ - org.openedx.content_authoring.content_library.collection.deleted.v1 - 2024-08-23 + + * - `CONTENT_OBJECT_ASSOCIATIONS_CHANGED `_ + - org.openedx.content_authoring.content.object.associations.changed.v1 + - 2024-09-06 diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index 76bb5eb3f4a4..4a775a710da6 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -34,6 +34,7 @@ searchable_doc_for_course_block, searchable_doc_for_collection, searchable_doc_for_library_block, + searchable_doc_collections, searchable_doc_tags, ) @@ -322,6 +323,9 @@ def rebuild_index(status_cb: Callable[[str], None] | None = None) -> None: Fields.tags + "." + Fields.tags_level1, Fields.tags + "." + Fields.tags_level2, Fields.tags + "." + Fields.tags_level3, + Fields.collections, + Fields.collections + "." + Fields.collections_display_name, + Fields.collections + "." + Fields.collections_key, Fields.type, Fields.access_id, Fields.last_published, @@ -333,8 +337,9 @@ def rebuild_index(status_cb: Callable[[str], None] | None = None) -> None: Fields.display_name, Fields.block_id, Fields.content, - Fields.tags, Fields.description, + Fields.tags, + Fields.collections, # If we don't list the following sub-fields _explicitly_, they're only sometimes searchable - that is, they # are searchable only if at least one document in the index has a value. If we didn't list them here and, # say, there were no tags.level3 tags in the index, the client would get an error if trying to search for @@ -344,6 +349,8 @@ def rebuild_index(status_cb: Callable[[str], None] | None = None) -> None: Fields.tags + "." + Fields.tags_level1, Fields.tags + "." + Fields.tags_level2, Fields.tags + "." + Fields.tags_level3, + Fields.collections + "." + Fields.collections_display_name, + Fields.collections + "." + Fields.collections_key, ]) # Mark which attributes can be used for sorting search results: client.index(temp_index_name).update_sortable_attributes([ @@ -375,6 +382,7 @@ def index_library(lib_key: str) -> list: doc = {} doc.update(searchable_doc_for_library_block(metadata)) doc.update(searchable_doc_tags(metadata.usage_key)) + doc.update(searchable_doc_collections(metadata.usage_key)) docs.append(doc) except Exception as err: # pylint: disable=broad-except status_cb(f"Error indexing library component {component}: {err}") @@ -549,6 +557,22 @@ def upsert_library_block_index_doc(usage_key: UsageKey) -> None: _update_index_docs(docs) +def upsert_library_collection_index_doc(library_key: LibraryLocatorV2, collection_key: str) -> None: + """ + Creates or updates the document for the given Library Collection in the search index + """ + content_library = lib_api.ContentLibrary.objects.get_by_key(library_key) + collection = authoring_api.get_collection( + learning_package_id=content_library.learning_package_id, + collection_key=collection_key, + ) + docs = [ + searchable_doc_for_collection(collection) + ] + + _update_index_docs(docs) + + def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None: """ Creates or updates the documents for the given Content Library in the search index @@ -571,6 +595,15 @@ def upsert_block_tags_index_docs(usage_key: UsageKey): _update_index_docs([doc]) +def upsert_block_collections_index_docs(usage_key: UsageKey): + """ + Updates the collections data in documents for the given Course/Library block + """ + doc = {Fields.id: meili_id_from_opaque_key(usage_key)} + doc.update(searchable_doc_collections(usage_key)) + _update_index_docs([doc]) + + def _get_user_orgs(request: Request) -> list[str]: """ Get the org.short_names for the organizations that the requesting user has OrgStaffRole or OrgInstructorRole. diff --git a/openedx/core/djangoapps/content/search/documents.py b/openedx/core/djangoapps/content/search/documents.py index a45b37ab2ad2..6f19b610fe86 100644 --- a/openedx/core/djangoapps/content/search/documents.py +++ b/openedx/core/djangoapps/content/search/documents.py @@ -7,7 +7,9 @@ from hashlib import blake2b from django.utils.text import slugify +from django.core.exceptions import ObjectDoesNotExist from opaque_keys.edx.keys import LearningContextKey, UsageKey +from openedx_learning.api import authoring as authoring_api from openedx.core.djangoapps.content.search.models import SearchAccess from openedx.core.djangoapps.content_libraries import api as lib_api @@ -26,7 +28,11 @@ class Fields: id = "id" usage_key = "usage_key" type = "type" # DocType.course_block or DocType.library_block (see below) - block_id = "block_id" # The block_id part of the usage key. Sometimes human-readable, sometimes a random hex ID + # The block_id part of the usage key for course or library blocks. + # If it's a collection, the collection.key is stored here. + # Sometimes human-readable, sometimes a random hex ID + # Is only unique within the given context_key. + block_id = "block_id" display_name = "display_name" description = "description" modified = "modified" @@ -52,11 +58,21 @@ class Fields: tags_level1 = "level1" tags_level2 = "level2" tags_level3 = "level3" + # Collections (dictionary) that this object belongs to. + # Similarly to tags above, we collect the collection.titles and collection.keys into hierarchical facets. + collections = "collections" + collections_display_name = "display_name" + collections_key = "key" + # The "content" field is a dictionary of arbitrary data, depending on the block_type. # It comes from each XBlock's index_dictionary() method (if present) plus some processing. # Text (html) blocks have an "html_content" key in here, capa has "capa_content" and "problem_types", and so on. content = "content" + # Collections use this field to communicate how many entities/components they contain. + # Structural XBlocks may use this one day to indicate how many child blocks they ocntain. + num_children = "num_children" + # Note: new fields or values can be added at any time, but if they need to be indexed for filtering or keyword # search, the index configuration will need to be changed, which is only done as part of the 'reindex_studio' # command (changing those settings on an large active index is not recommended). @@ -223,6 +239,51 @@ def _tags_for_content_object(object_id: UsageKey | LearningContextKey) -> dict: return {Fields.tags: result} +def _collections_for_content_object(object_id: UsageKey | LearningContextKey) -> dict: + """ + Given an XBlock, course, library, etc., get the collections for its index doc. + + e.g. for something in Collections "COL_A" and "COL_B", this would return: + { + "collections": { + "display_name": ["Collection A", "Collection B"], + "key": ["COL_A", "COL_B"], + } + } + + If the object is in no collections, returns: + { + "collections": {}, + } + + """ + # Gather the collections associated with this object + collections = None + try: + component = lib_api.get_component_from_usage_key(object_id) + collections = authoring_api.get_entity_collections( + component.learning_package_id, + component.key, + ) + except ObjectDoesNotExist: + log.warning(f"No component found for {object_id}") + + if not collections: + return {Fields.collections: {}} + + result = { + Fields.collections: { + Fields.collections_display_name: [], + Fields.collections_key: [], + } + } + for collection in collections: + result[Fields.collections][Fields.collections_display_name].append(collection.title) + result[Fields.collections][Fields.collections_key].append(collection.key) + + return result + + def searchable_doc_for_library_block(xblock_metadata: lib_api.LibraryXBlockMetadata) -> dict: """ Generate a dictionary document suitable for ingestion into a search engine @@ -265,6 +326,19 @@ def searchable_doc_tags(usage_key: UsageKey) -> dict: return doc +def searchable_doc_collections(usage_key: UsageKey) -> dict: + """ + Generate a dictionary document suitable for ingestion into a search engine + like Meilisearch or Elasticsearch, with the collections data for the given content object. + """ + doc = { + Fields.id: meili_id_from_opaque_key(usage_key), + } + doc.update(_collections_for_content_object(usage_key)) + + return doc + + def searchable_doc_for_course_block(block) -> dict: """ Generate a dictionary document suitable for ingestion into a search engine @@ -289,6 +363,7 @@ def searchable_doc_for_collection(collection) -> dict: """ doc = { Fields.id: collection.id, + Fields.block_id: collection.key, Fields.type: DocType.collection, Fields.display_name: collection.title, Fields.description: collection.description, @@ -298,6 +373,7 @@ def searchable_doc_for_collection(collection) -> dict: # If related contentlibrary is found, it will override this value below. # Mostly contentlibrary.library_key == learning_package.key Fields.context_key: collection.learning_package.key, + Fields.num_children: collection.entities.count(), } # Just in case learning_package is not related to a library try: diff --git a/openedx/core/djangoapps/content/search/handlers.py b/openedx/core/djangoapps/content/search/handlers.py index 94ac02ea1606..6a341c92ed2b 100644 --- a/openedx/core/djangoapps/content/search/handlers.py +++ b/openedx/core/djangoapps/content/search/handlers.py @@ -6,30 +6,40 @@ from django.db.models.signals import post_delete from django.dispatch import receiver -from openedx_events.content_authoring.data import ContentLibraryData, ContentObjectData, LibraryBlockData, XBlockData +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import UsageKey +from openedx_events.content_authoring.data import ( + ContentLibraryData, + ContentObjectChangedData, + LibraryBlockData, + LibraryCollectionData, + XBlockData, +) from openedx_events.content_authoring.signals import ( CONTENT_LIBRARY_DELETED, CONTENT_LIBRARY_UPDATED, LIBRARY_BLOCK_CREATED, LIBRARY_BLOCK_DELETED, LIBRARY_BLOCK_UPDATED, + LIBRARY_COLLECTION_CREATED, + LIBRARY_COLLECTION_UPDATED, XBLOCK_CREATED, XBLOCK_DELETED, XBLOCK_UPDATED, - CONTENT_OBJECT_TAGS_CHANGED, + CONTENT_OBJECT_ASSOCIATIONS_CHANGED, ) -from openedx.core.djangoapps.content_tagging.utils import get_content_key_from_string from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.search.models import SearchAccess -from .api import only_if_meilisearch_enabled, upsert_block_tags_index_docs +from .api import only_if_meilisearch_enabled, upsert_block_collections_index_docs, upsert_block_tags_index_docs from .tasks import ( delete_library_block_index_doc, delete_xblock_index_doc, update_content_library_index_docs, + update_library_collection_index_doc, upsert_library_block_index_doc, - upsert_xblock_index_doc + upsert_xblock_index_doc, ) log = logging.getLogger(__name__) @@ -145,22 +155,48 @@ def content_library_updated_handler(**kwargs) -> None: update_content_library_index_docs.apply(args=[str(content_library_data.library_key)]) -@receiver(CONTENT_OBJECT_TAGS_CHANGED) +@receiver(LIBRARY_COLLECTION_CREATED) +@receiver(LIBRARY_COLLECTION_UPDATED) +@only_if_meilisearch_enabled +def library_collection_updated_handler(**kwargs) -> None: + """ + Create or update the index for the content library collection + """ + library_collection = kwargs.get("library_collection", None) + if not library_collection or not isinstance(library_collection, LibraryCollectionData): # pragma: no cover + log.error("Received null or incorrect data for event") + return + + # Update collection index synchronously to make sure that search index is updated before + # the frontend invalidates/refetches index. + # See content_library_updated_handler for more details. + update_library_collection_index_doc.apply(args=[ + str(library_collection.library_key), + library_collection.collection_key, + ]) + + +@receiver(CONTENT_OBJECT_ASSOCIATIONS_CHANGED) @only_if_meilisearch_enabled -def content_object_tags_changed_handler(**kwargs) -> None: +def content_object_associations_changed_handler(**kwargs) -> None: """ - Update the tags data in the index for the Content Object + Update the collections/tags data in the index for the Content Object """ - content_object_tags = kwargs.get("content_object", None) - if not content_object_tags or not isinstance(content_object_tags, ContentObjectData): + content_object = kwargs.get("content_object", None) + if not content_object or not isinstance(content_object, ContentObjectChangedData): log.error("Received null or incorrect data for event") return try: # Check if valid if course or library block - get_content_key_from_string(content_object_tags.object_id) - except ValueError: + usage_key = UsageKey.from_string(str(content_object.object_id)) + except InvalidKeyError: log.error("Received invalid content object id") return - upsert_block_tags_index_docs(content_object_tags.object_id) + # This event's changes may contain both "tags" and "collections", but this will happen rarely, if ever. + # So we allow a potential double "upsert" here. + if not content_object.changes or "tags" in content_object.changes: + upsert_block_tags_index_docs(usage_key) + if not content_object.changes or "collections" in content_object.changes: + upsert_block_collections_index_docs(usage_key) diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index dfd603776981..d9dad834db29 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -84,3 +84,16 @@ def update_content_library_index_docs(library_key_str: str) -> None: # Delete all documents in this library that were not published by above function # as this task is also triggered on discard event. api.delete_all_draft_docs_for_library(library_key) + + +@shared_task(base=LoggedTask, autoretry_for=(MeilisearchError, ConnectionError)) +@set_code_owner_attribute +def update_library_collection_index_doc(library_key_str: str, collection_key: str) -> None: + """ + Celery task to update the content index documents for a library collection + """ + library_key = LibraryLocatorV2.from_string(library_key_str) + + log.info("Updating content index documents for collection %s in library%s", collection_key, library_key) + + api.upsert_library_collection_index_doc(library_key, collection_key) diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index d6b58674f5ec..023265f4d0f5 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -186,16 +186,18 @@ def setUp(self): description="my collection description" ) self.collection_dict = { - 'id': self.collection.id, - 'type': 'collection', - 'display_name': 'my_collection', - 'description': 'my collection description', - 'context_key': 'lib:org1:lib', - 'org': 'org1', - 'created': created_date.timestamp(), - 'modified': created_date.timestamp(), + "id": self.collection.id, + "block_id": self.collection.key, + "type": "collection", + "display_name": "my_collection", + "description": "my collection description", + "num_children": 0, + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": created_date.timestamp(), "access_id": lib_access.id, - 'breadcrumbs': [{'display_name': 'Library'}] + "breadcrumbs": [{"display_name": "Library"}], } @override_settings(MEILISEARCH_ENABLED=False) @@ -215,10 +217,13 @@ def test_reindex_meilisearch(self, mock_meilisearch): doc_vertical["tags"] = {} doc_problem1 = copy.deepcopy(self.doc_problem1) doc_problem1["tags"] = {} + doc_problem1["collections"] = {} doc_problem2 = copy.deepcopy(self.doc_problem2) doc_problem2["tags"] = {} + doc_problem2["collections"] = {} api.rebuild_index() + assert mock_meilisearch.return_value.index.return_value.add_documents.call_count == 3 mock_meilisearch.return_value.index.return_value.add_documents.assert_has_calls( [ call([doc_sequential, doc_vertical]), @@ -254,6 +259,7 @@ def test_reindex_meilisearch_library_block_error(self, mock_meilisearch): doc_vertical["tags"] = {} doc_problem2 = copy.deepcopy(self.doc_problem2) doc_problem2["tags"] = {} + doc_problem2["collections"] = {} orig_from_component = library_api.LibraryXBlockMetadata.from_component @@ -346,6 +352,7 @@ def test_index_xblock_tags(self, mock_meilisearch): } } + assert mock_meilisearch.return_value.index.return_value.update_documents.call_count == 2 mock_meilisearch.return_value.index.return_value.update_documents.assert_has_calls( [ call([doc_sequential_with_tags1]), @@ -400,6 +407,7 @@ def test_index_library_block_tags(self, mock_meilisearch): } } + assert mock_meilisearch.return_value.index.return_value.update_documents.call_count == 2 mock_meilisearch.return_value.index.return_value.update_documents.assert_has_calls( [ call([doc_problem_with_tags1]), @@ -408,6 +416,130 @@ def test_index_library_block_tags(self, mock_meilisearch): any_order=True, ) + @override_settings(MEILISEARCH_ENABLED=True) + def test_index_library_block_and_collections(self, mock_meilisearch): + """ + Test indexing an Library Block and the Collections it's in. + """ + # Create collections (these internally call `upsert_library_collection_index_doc`) + created_date = datetime(2023, 5, 6, 7, 8, 9, tzinfo=timezone.utc) + with freeze_time(created_date): + collection1 = library_api.create_library_collection( + self.library.key, + collection_key="COL1", + title="Collection 1", + created_by=None, + description="First Collection", + ) + + collection2 = library_api.create_library_collection( + self.library.key, + collection_key="COL2", + title="Collection 2", + created_by=None, + description="Second Collection", + ) + + # Add Problem1 to both Collections (these internally call `upsert_block_collections_index_docs` and + # `upsert_library_collection_index_doc`) + # (adding in reverse order to test sorting of collection tag) + updated_date = datetime(2023, 6, 7, 8, 9, 10, tzinfo=timezone.utc) + with freeze_time(updated_date): + for collection in (collection2, collection1): + library_api.update_library_collection_components( + self.library.key, + collection_key=collection.key, + usage_keys=[ + self.problem1.usage_key, + ], + ) + + # Build expected docs at each stage + lib_access, _ = SearchAccess.objects.get_or_create(context_key=self.library.key) + doc_collection1_created = { + "id": collection1.id, + "block_id": collection1.key, + "type": "collection", + "display_name": "Collection 1", + "description": "First Collection", + "num_children": 0, + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": created_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } + doc_collection2_created = { + "id": collection2.id, + "block_id": collection2.key, + "type": "collection", + "display_name": "Collection 2", + "description": "Second Collection", + "num_children": 0, + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": created_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } + doc_collection2_updated = { + "id": collection2.id, + "block_id": collection2.key, + "type": "collection", + "display_name": "Collection 2", + "description": "Second Collection", + "num_children": 1, + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": updated_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } + doc_collection1_updated = { + "id": collection1.id, + "block_id": collection1.key, + "type": "collection", + "display_name": "Collection 1", + "description": "First Collection", + "num_children": 1, + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": updated_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } + doc_problem_with_collection1 = { + "id": self.doc_problem1["id"], + "collections": { + "display_name": ["Collection 2"], + "key": ["COL2"], + }, + } + doc_problem_with_collection2 = { + "id": self.doc_problem1["id"], + "collections": { + "display_name": ["Collection 1", "Collection 2"], + "key": ["COL1", "COL2"], + }, + } + + assert mock_meilisearch.return_value.index.return_value.update_documents.call_count == 6 + mock_meilisearch.return_value.index.return_value.update_documents.assert_has_calls( + [ + call([doc_collection1_created]), + call([doc_collection2_created]), + call([doc_collection2_updated]), + call([doc_collection1_updated]), + call([doc_problem_with_collection1]), + call([doc_problem_with_collection2]), + ], + any_order=True, + ) + @override_settings(MEILISEARCH_ENABLED=True) def test_delete_index_library_block(self, mock_meilisearch): """ diff --git a/openedx/core/djangoapps/content/search/tests/test_documents.py b/openedx/core/djangoapps/content/search/tests/test_documents.py index 1f10fc3c988f..7ff330c0b491 100644 --- a/openedx/core/djangoapps/content/search/tests/test_documents.py +++ b/openedx/core/djangoapps/content/search/tests/test_documents.py @@ -8,6 +8,7 @@ from openedx_learning.api import authoring as authoring_api from openedx.core.djangoapps.content_tagging import api as tagging_api +from openedx.core.djangoapps.content_libraries import api as library_api from openedx.core.djangolib.testing.utils import skip_unless_cms from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase @@ -15,12 +16,19 @@ try: # This import errors in the lms because content.search is not an installed app there. - from ..documents import searchable_doc_for_course_block, searchable_doc_tags, searchable_doc_for_collection + from ..documents import ( + searchable_doc_for_course_block, + searchable_doc_tags, + searchable_doc_collections, + searchable_doc_for_collection, + searchable_doc_for_library_block, + ) from ..models import SearchAccess except RuntimeError: searchable_doc_for_course_block = lambda x: x searchable_doc_tags = lambda x: x searchable_doc_for_collection = lambda x: x + searchable_doc_for_library_block = lambda x: x SearchAccess = {} @@ -38,12 +46,13 @@ class StudioDocumentsTest(SharedModuleStoreTestCase): def setUpClass(cls): super().setUpClass() cls.store = modulestore() + cls.org = Organization.objects.create(name="edX", short_name="edX") cls.toy_course = ToyCourseFactory.create() # See xmodule/modulestore/tests/sample_courses.py cls.toy_course_key = cls.toy_course.id # Get references to some blocks in the toy course cls.html_block_key = cls.toy_course_key.make_usage_key("html", "toyjumpto") - # Create a problem in library + # Create a problem in course cls.problem_block = BlockFactory.create( category="problem", parent_location=cls.toy_course_key.make_usage_key("vertical", "vertical_test"), @@ -51,8 +60,38 @@ def setUpClass(cls): data="What is a test?", ) + # Create a library and collection with a block + created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc) + with freeze_time(created_date): + cls.library = library_api.create_library( + org=cls.org, + slug="2012_Fall", + title="some content_library", + description="some description", + ) + cls.collection = library_api.create_library_collection( + cls.library.key, + collection_key="TOY_COLLECTION", + title="Toy Collection", + created_by=None, + description="my toy collection description" + ) + cls.library_block = library_api.create_library_block( + cls.library.key, + "html", + "text2", + ) + + # Add the problem block to the collection + library_api.update_library_collection_components( + cls.library.key, + collection_key="TOY_COLLECTION", + usage_keys=[ + cls.library_block.usage_key, + ] + ) + # Create a couple taxonomies and some tags - cls.org = Organization.objects.create(name="edX", short_name="edX") cls.difficulty_tags = tagging_api.create_taxonomy(name="Difficulty", orgs=[cls.org], allow_multiple=False) tagging_api.add_tag_to_taxonomy(cls.difficulty_tags, tag="Easy") tagging_api.add_tag_to_taxonomy(cls.difficulty_tags, tag="Normal") @@ -69,6 +108,7 @@ def setUpClass(cls): tagging_api.tag_object(str(cls.problem_block.usage_key), cls.difficulty_tags, tags=["Easy"]) tagging_api.tag_object(str(cls.html_block_key), cls.subject_tags, tags=["Chinese", "Jump Links"]) tagging_api.tag_object(str(cls.html_block_key), cls.difficulty_tags, tags=["Normal"]) + tagging_api.tag_object(str(cls.library_block.usage_key), cls.difficulty_tags, tags=["Normal"]) @property def toy_course_access_id(self): @@ -80,6 +120,16 @@ def toy_course_access_id(self): """ return SearchAccess.objects.get(context_key=self.toy_course_key).id + @property + def library_access_id(self): + """ + Returns the SearchAccess.id created for the library. + + This SearchAccess object is created when documents are added to the search index, so this method must be called + after this step, or risk a DoesNotExist error. + """ + return SearchAccess.objects.get(context_key=self.library.key).id + def test_problem_block(self): """ Test how a problem block gets represented in the search index @@ -205,6 +255,62 @@ def test_video_block_untagged(self): # This video has no tags. } + def test_html_library_block(self): + """ + Test how a library block gets represented in the search index + """ + doc = {} + doc.update(searchable_doc_for_library_block(self.library_block)) + doc.update(searchable_doc_tags(self.library_block.usage_key)) + doc.update(searchable_doc_collections(self.library_block.usage_key)) + assert doc == { + "id": "lbedx2012_fallhtmltext2-4bb47d67", + "type": "library_block", + "block_type": "html", + "usage_key": "lb:edX:2012_Fall:html:text2", + "block_id": "text2", + "context_key": "lib:edX:2012_Fall", + "org": "edX", + "access_id": self.library_access_id, + "display_name": "Text", + "breadcrumbs": [ + { + "display_name": "some content_library", + }, + ], + "last_published": None, + "created": 1680674828.0, + "modified": 1680674828.0, + "content": { + "html_content": "", + }, + "collections": { + "key": ["TOY_COLLECTION"], + "display_name": ["Toy Collection"], + }, + "tags": { + "taxonomy": ["Difficulty"], + "level0": ["Difficulty > Normal"], + }, + } + + def test_collection_with_library(self): + doc = searchable_doc_for_collection(self.collection) + assert doc == { + "id": self.collection.id, + "block_id": self.collection.key, + "type": "collection", + "org": "edX", + "display_name": "Toy Collection", + "description": "my toy collection description", + "num_children": 1, + "context_key": "lib:edX:2012_Fall", + "access_id": self.library_access_id, + "breadcrumbs": [{"display_name": "some content_library"}], + "created": 1680674828.0, + "modified": 1680674828.0, + } + def test_collection_with_no_library(self): created_date = datetime(2023, 4, 5, 6, 7, 8, tzinfo=timezone.utc) with freeze_time(created_date): @@ -223,9 +329,11 @@ def test_collection_with_no_library(self): doc = searchable_doc_for_collection(collection) assert doc == { "id": collection.id, + "block_id": collection.key, "type": "collection", "display_name": "my_collection", "description": "my collection description", + "num_children": 0, "context_key": learning_package.key, "access_id": self.toy_course_access_id, "breadcrumbs": [{"display_name": "some learning_package"}], diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 00110143456a..c19c9bf880d0 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -78,12 +78,12 @@ from opaque_keys import InvalidKeyError from openedx_events.content_authoring.data import ( ContentLibraryData, - ContentObjectData, + ContentObjectChangedData, LibraryBlockData, LibraryCollectionData, ) from openedx_events.content_authoring.signals import ( - CONTENT_OBJECT_TAGS_CHANGED, + CONTENT_OBJECT_ASSOCIATIONS_CHANGED, CONTENT_LIBRARY_CREATED, CONTENT_LIBRARY_DELETED, CONTENT_LIBRARY_UPDATED, @@ -1235,10 +1235,13 @@ def update_library_collection_components( ) ) - # Emit a CONTENT_OBJECT_TAGS_CHANGED event for each of the objects added/removed + # Emit a CONTENT_OBJECT_ASSOCIATIONS_CHANGED event for each of the objects added/removed for usage_key in usage_keys: - CONTENT_OBJECT_TAGS_CHANGED.send_event( - content_object=ContentObjectData(object_id=usage_key), + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( + content_object=ContentObjectChangedData( + object_id=str(usage_key), + changes=["collections"], + ), ) return collection diff --git a/openedx/core/djangoapps/content_libraries/tests/test_api.py b/openedx/core/djangoapps/content_libraries/tests/test_api.py index b7b646acac56..b02e71b002a3 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_api.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_api.py @@ -14,11 +14,11 @@ ) from opaque_keys.edx.locator import LibraryLocatorV2 from openedx_events.content_authoring.data import ( - ContentObjectData, + ContentObjectChangedData, LibraryCollectionData, ) from openedx_events.content_authoring.signals import ( - CONTENT_OBJECT_TAGS_CHANGED, + CONTENT_OBJECT_ASSOCIATIONS_CHANGED, LIBRARY_COLLECTION_CREATED, LIBRARY_COLLECTION_UPDATED, ) @@ -262,7 +262,7 @@ class ContentLibraryCollectionsTest(ContentLibrariesRestApiTest, OpenEdxEventsTe Same guidelines as ContentLibrariesTestCase. """ ENABLED_OPENEDX_EVENTS = [ - CONTENT_OBJECT_TAGS_CHANGED.event_type, + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.event_type, LIBRARY_COLLECTION_CREATED.event_type, LIBRARY_COLLECTION_UPDATED.event_type, ] @@ -411,10 +411,10 @@ def test_update_library_collection_components(self): def test_update_library_collection_components_event(self): """ - Check that a CONTENT_OBJECT_TAGS_CHANGED event is raised for each added/removed component. + Check that a CONTENT_OBJECT_ASSOCIATIONS_CHANGED event is raised for each added/removed component. """ event_receiver = mock.Mock() - CONTENT_OBJECT_TAGS_CHANGED.connect(event_receiver) + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.connect(event_receiver) LIBRARY_COLLECTION_UPDATED.connect(event_receiver) api.update_library_collection_components( @@ -440,20 +440,22 @@ def test_update_library_collection_components_event(self): ) self.assertDictContainsSubset( { - "signal": CONTENT_OBJECT_TAGS_CHANGED, + "signal": CONTENT_OBJECT_ASSOCIATIONS_CHANGED, "sender": None, - "content_object": ContentObjectData( - object_id=UsageKey.from_string(self.lib1_problem_block["id"]), + "content_object": ContentObjectChangedData( + object_id=self.lib1_problem_block["id"], + changes=["collections"], ), }, event_receiver.call_args_list[1].kwargs, ) self.assertDictContainsSubset( { - "signal": CONTENT_OBJECT_TAGS_CHANGED, + "signal": CONTENT_OBJECT_ASSOCIATIONS_CHANGED, "sender": None, - "content_object": ContentObjectData( - object_id=UsageKey.from_string(self.lib1_html_block["id"]), + "content_object": ContentObjectChangedData( + object_id=self.lib1_html_block["id"], + changes=["collections"], ), }, event_receiver.call_args_list[2].kwargs, diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index 47b157c1a34e..b63c70c5f0b8 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -18,8 +18,11 @@ from openedx_tagging.core.tagging.models.utils import TAGS_CSV_SEPARATOR from organizations.models import Organization from .helpers.objecttag_export_helpers import build_object_tree_with_objecttags, iterate_with_level -from openedx_events.content_authoring.data import ContentObjectData -from openedx_events.content_authoring.signals import CONTENT_OBJECT_TAGS_CHANGED +from openedx_events.content_authoring.data import ContentObjectData, ContentObjectChangedData +from openedx_events.content_authoring.signals import ( + CONTENT_OBJECT_ASSOCIATIONS_CHANGED, + CONTENT_OBJECT_TAGS_CHANGED, +) from .models import TaxonomyOrg from .types import ContentKey, TagValuesByObjectIdDict, TagValuesByTaxonomyIdDict, TaxonomyDict @@ -301,6 +304,16 @@ def set_exported_object_tags( create_invalid=True, taxonomy_export_id=str(taxonomy_export_id), ) + + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( + time=now(), + content_object=ContentObjectChangedData( + object_id=content_key_str, + changes=["tags"], + ) + ) + + # Emit a (deprecated) CONTENT_OBJECT_TAGS_CHANGED event too CONTENT_OBJECT_TAGS_CHANGED.send_event( time=now(), content_object=ContentObjectData(object_id=content_key_str) @@ -378,7 +391,7 @@ def tag_object( Replaces the existing ObjectTag entries for the given taxonomy + object_id with the given list of tags, if the taxonomy can be used by the given object_id. - This is a wrapper around oel_tagging.tag_object that adds emitting the `CONTENT_OBJECT_TAGS_CHANGED` event + This is a wrapper around oel_tagging.tag_object that adds emitting the `CONTENT_OBJECT_ASSOCIATIONS_CHANGED` event when tagging an object. tags: A list of the values of the tags from this taxonomy to apply. @@ -399,6 +412,15 @@ def tag_object( taxonomy=taxonomy, tags=tags, ) + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( + time=now(), + content_object=ContentObjectChangedData( + object_id=object_id, + changes=["tags"], + ) + ) + + # Emit a (deprecated) CONTENT_OBJECT_TAGS_CHANGED event too CONTENT_OBJECT_TAGS_CHANGED.send_event( time=now(), content_object=ContentObjectData(object_id=object_id) diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py index 71b210b9e561..3fc99736bae9 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/views.py @@ -13,8 +13,11 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from openedx_events.content_authoring.data import ContentObjectData -from openedx_events.content_authoring.signals import CONTENT_OBJECT_TAGS_CHANGED +from openedx_events.content_authoring.data import ContentObjectData, ContentObjectChangedData +from openedx_events.content_authoring.signals import ( + CONTENT_OBJECT_ASSOCIATIONS_CHANGED, + CONTENT_OBJECT_TAGS_CHANGED, +) from ...auth import has_view_object_tags_access from ...api import ( @@ -149,14 +152,24 @@ class ObjectTagOrgView(ObjectTagView): def update(self, request, *args, **kwargs) -> Response: """ - Extend the update method to fire CONTENT_OBJECT_TAGS_CHANGED event + Extend the update method to fire CONTENT_OBJECT_ASSOCIATIONS_CHANGED event """ response = super().update(request, *args, **kwargs) if response.status_code == 200: object_id = kwargs.get('object_id') + + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( + content_object=ContentObjectChangedData( + object_id=object_id, + changes=["tags"], + ) + ) + + # Emit a (deprecated) CONTENT_OBJECT_TAGS_CHANGED event too CONTENT_OBJECT_TAGS_CHANGED.send_event( content_object=ContentObjectData(object_id=object_id) ) + return response From dd59dc634a56a57449a8254d2b8bce294bae280a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Fri, 13 Sep 2024 09:19:25 -0500 Subject: [PATCH 7/7] Tag in Collections [FC-0062] (#35383) * feat: Allow tag in collections * chore: Bump version of opaque-keys --- .../core/djangoapps/content_tagging/api.py | 4 +- .../rest_api/v1/tests/test_views.py | 91 ++++++++++++++++++- .../content_tagging/tests/test_api.py | 19 +++- .../core/djangoapps/content_tagging/types.py | 4 +- .../core/djangoapps/content_tagging/utils.py | 16 +++- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/doc.txt | 2 +- requirements/edx/paver.txt | 2 +- requirements/edx/testing.txt | 2 +- 10 files changed, 130 insertions(+), 14 deletions(-) diff --git a/openedx/core/djangoapps/content_tagging/api.py b/openedx/core/djangoapps/content_tagging/api.py index b63c70c5f0b8..f015770e5db8 100644 --- a/openedx/core/djangoapps/content_tagging/api.py +++ b/openedx/core/djangoapps/content_tagging/api.py @@ -12,7 +12,7 @@ import openedx_tagging.core.tagging.api as oel_tagging from django.db.models import Exists, OuterRef, Q, QuerySet from django.utils.timezone import now -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, LibraryCollectionKey from opaque_keys.edx.locator import LibraryLocatorV2 from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy from openedx_tagging.core.tagging.models.utils import TAGS_CSV_SEPARATOR @@ -230,7 +230,7 @@ def generate_csv_rows(object_id, buffer) -> Iterator[str]: """ content_key = get_content_key_from_string(object_id) - if isinstance(content_key, UsageKey): + if isinstance(content_key, (UsageKey, LibraryCollectionKey)): raise ValueError("The object_id must be a CourseKey or a LibraryLocatorV2.") all_object_tags, taxonomies = get_all_object_tags(content_key) diff --git a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py index f64fba5c3358..e386ee234226 100644 --- a/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py +++ b/openedx/core/djangoapps/content_tagging/rest_api/v1/tests/test_views.py @@ -13,7 +13,7 @@ import ddt from django.contrib.auth import get_user_model from django.core.files.uploadedfile import SimpleUploadedFile -from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator +from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryCollectionLocator from openedx_tagging.core.tagging.models import Tag, Taxonomy from openedx_tagging.core.tagging.models.system_defined import SystemDefinedTaxonomy from openedx_tagging.core.tagging.rest_api.v1.serializers import TaxonomySerializer @@ -111,6 +111,9 @@ def _setUp_library(self): ) self.libraryA = str(self.content_libraryA.key) + def _setUp_collection(self): + self.collection_key = str(LibraryCollectionLocator(self.content_libraryA.key, 'test-collection')) + def _setUp_users(self): """ Create users for testing @@ -284,6 +287,7 @@ def setUp(self): self._setUp_library() self._setUp_users() self._setUp_taxonomies() + self._setUp_collection() # Clear the rules cache in between test runs to keep query counts consistent. rules_cache.clear() @@ -1653,6 +1657,87 @@ def test_tag_library_invalid(self, user_attr, taxonomy_attr): response = self._call_put_request(self.libraryA, taxonomy.pk, ["invalid"]) assert response.status_code == status.HTTP_400_BAD_REQUEST + @ddt.data( + # staffA and staff are staff in collection and can tag using enabled taxonomies + ("user", "tA1", ["Tag 1"], status.HTTP_403_FORBIDDEN), + ("staffA", "tA1", ["Tag 1"], status.HTTP_200_OK), + ("staff", "tA1", ["Tag 1"], status.HTTP_200_OK), + ("user", "tA1", [], status.HTTP_403_FORBIDDEN), + ("staffA", "tA1", [], status.HTTP_200_OK), + ("staff", "tA1", [], status.HTTP_200_OK), + ("user", "multiple_taxonomy", ["Tag 1", "Tag 2"], status.HTTP_403_FORBIDDEN), + ("staffA", "multiple_taxonomy", ["Tag 1", "Tag 2"], status.HTTP_200_OK), + ("staff", "multiple_taxonomy", ["Tag 1", "Tag 2"], status.HTTP_200_OK), + ("user", "open_taxonomy", ["tag1"], status.HTTP_403_FORBIDDEN), + ("staffA", "open_taxonomy", ["tag1"], status.HTTP_200_OK), + ("staff", "open_taxonomy", ["tag1"], status.HTTP_200_OK), + ) + @ddt.unpack + def test_tag_collection(self, user_attr, taxonomy_attr, tag_values, expected_status): + """ + Tests that only staff and org level users can tag collections + """ + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + taxonomy = getattr(self, taxonomy_attr) + + response = self._call_put_request(self.collection_key, taxonomy.pk, tag_values) + + assert response.status_code == expected_status + if status.is_success(expected_status): + tags_by_taxonomy = response.data[str(self.collection_key)]["taxonomies"] + if tag_values: + response_taxonomy = tags_by_taxonomy[0] + assert response_taxonomy["name"] == taxonomy.name + response_tags = response_taxonomy["tags"] + assert [t["value"] for t in response_tags] == tag_values + else: + assert tags_by_taxonomy == [] # No tags are set from any taxonomy + + # Check that re-fetching the tags returns what we set + url = OBJECT_TAG_UPDATE_URL.format(object_id=self.collection_key) + new_response = self.client.get(url, format="json") + assert status.is_success(new_response.status_code) + assert new_response.data == response.data + + @ddt.data( + "staffA", + "staff", + ) + def test_tag_collection_disabled_taxonomy(self, user_attr): + """ + Nobody can use disabled taxonomies to tag objects + """ + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + disabled_taxonomy = self.tA2 + assert disabled_taxonomy.enabled is False + + response = self._call_put_request(self.collection_key, disabled_taxonomy.pk, ["Tag 1"]) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + @ddt.data( + ("staffA", "tA1"), + ("staff", "tA1"), + ("staffA", "multiple_taxonomy"), + ("staff", "multiple_taxonomy"), + ) + @ddt.unpack + def test_tag_collection_invalid(self, user_attr, taxonomy_attr): + """ + Tests that nobody can add invalid tags to a collection using a closed taxonomy + """ + user = getattr(self, user_attr) + self.client.force_authenticate(user=user) + + taxonomy = getattr(self, taxonomy_attr) + + response = self._call_put_request(self.collection_key, taxonomy.pk, ["invalid"]) + assert response.status_code == status.HTTP_400_BAD_REQUEST + @ddt.data( ("superuser", status.HTTP_200_OK), ("staff", status.HTTP_403_FORBIDDEN), @@ -1768,10 +1853,14 @@ def test_get_tags(self): @ddt.data( ('staff', 'courseA', 8), ('staff', 'libraryA', 8), + ('staff', 'collection_key', 8), ("content_creatorA", 'courseA', 11, False), ("content_creatorA", 'libraryA', 11, False), + ("content_creatorA", 'collection_key', 11, False), ("library_staffA", 'libraryA', 11, False), # Library users can only view objecttags, not change them? + ("library_staffA", 'collection_key', 11, False), ("library_userA", 'libraryA', 11, False), + ("library_userA", 'collection_key', 11, False), ("instructorA", 'courseA', 11), ("course_instructorA", 'courseA', 11), ("course_staffA", 'courseA', 11), diff --git a/openedx/core/djangoapps/content_tagging/tests/test_api.py b/openedx/core/djangoapps/content_tagging/tests/test_api.py index 1bc80b73727a..b693f7ee0f56 100644 --- a/openedx/core/djangoapps/content_tagging/tests/test_api.py +++ b/openedx/core/djangoapps/content_tagging/tests/test_api.py @@ -5,7 +5,7 @@ import ddt from django.test.testcases import TestCase from fs.osfs import OSFS -from opaque_keys.edx.keys import CourseKey, UsageKey +from opaque_keys.edx.keys import CourseKey, UsageKey, LibraryCollectionKey from opaque_keys.edx.locator import LibraryLocatorV2 from openedx_tagging.core.tagging.models import ObjectTag from organizations.models import Organization @@ -380,6 +380,23 @@ def test_copy_cross_org_tags(self): with self.assertNumQueries(31): # TODO why so high? self._test_copy_object_tags(src_key, dst_key, expected_tags) + def test_tag_collection(self): + collection_key = LibraryCollectionKey.from_string("lib-collection:orgA:libX:1") + + api.tag_object( + object_id=str(collection_key), + taxonomy=self.taxonomy_3, + tags=["Tag 3.1"], + ) + + with self.assertNumQueries(1): + object_tags, taxonomies = api.get_all_object_tags(collection_key) + + assert object_tags == {'lib-collection:orgA:libX:1': {3: ['Tag 3.1']}} + assert taxonomies == { + self.taxonomy_3.id: self.taxonomy_3, + } + class TestExportImportTags(TaggedCourseMixin): """ diff --git a/openedx/core/djangoapps/content_tagging/types.py b/openedx/core/djangoapps/content_tagging/types.py index 64fa0d58f000..9ffb090d61e3 100644 --- a/openedx/core/djangoapps/content_tagging/types.py +++ b/openedx/core/djangoapps/content_tagging/types.py @@ -5,11 +5,11 @@ from typing import Dict, List, Union -from opaque_keys.edx.keys import CourseKey, UsageKey +from opaque_keys.edx.keys import CourseKey, UsageKey, LibraryCollectionKey from opaque_keys.edx.locator import LibraryLocatorV2 from openedx_tagging.core.tagging.models import Taxonomy -ContentKey = Union[LibraryLocatorV2, CourseKey, UsageKey] +ContentKey = Union[LibraryLocatorV2, CourseKey, UsageKey, LibraryCollectionKey] ContextKey = Union[LibraryLocatorV2, CourseKey] TagValuesByTaxonomyIdDict = Dict[int, List[str]] diff --git a/openedx/core/djangoapps/content_tagging/utils.py b/openedx/core/djangoapps/content_tagging/utils.py index 8cc9c9e7f7a9..39dd925c1acd 100644 --- a/openedx/core/djangoapps/content_tagging/utils.py +++ b/openedx/core/djangoapps/content_tagging/utils.py @@ -5,7 +5,7 @@ from edx_django_utils.cache import RequestCache from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey, UsageKey +from opaque_keys.edx.keys import CourseKey, UsageKey, LibraryCollectionKey from opaque_keys.edx.locator import LibraryLocatorV2 from openedx_tagging.core.tagging.models import Taxonomy from organizations.models import Organization @@ -26,8 +26,14 @@ def get_content_key_from_string(key_str: str) -> ContentKey: except InvalidKeyError: try: return UsageKey.from_string(key_str) - except InvalidKeyError as usage_key_error: - raise ValueError("object_id must be a CourseKey, LibraryLocatorV2 or a UsageKey") from usage_key_error + except InvalidKeyError: + try: + return LibraryCollectionKey.from_string(key_str) + except InvalidKeyError as usage_key_error: + raise ValueError( + "object_id must be one of the following " + "keys: CourseKey, LibraryLocatorV2, UsageKey or LibCollectionKey" + ) from usage_key_error def get_context_key_from_key(content_key: ContentKey) -> ContextKey: @@ -38,6 +44,10 @@ def get_context_key_from_key(content_key: ContentKey) -> ContextKey: if isinstance(content_key, (CourseKey, LibraryLocatorV2)): return content_key + # If the content key is a LibraryCollectionKey, return the LibraryLocatorV2 + if isinstance(content_key, LibraryCollectionKey): + return content_key.library_key + # If the content key is a UsageKey, return the context key context_key = content_key.context_key diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 9c85f60fc3a8..8127bc618058 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -484,7 +484,7 @@ edx-milestones==0.6.0 # via -r requirements/edx/kernel.in edx-name-affirmation==2.4.0 # via -r requirements/edx/kernel.in -edx-opaque-keys[django]==2.10.0 +edx-opaque-keys[django]==2.11.0 # via # -r requirements/edx/kernel.in # -r requirements/edx/paver.txt diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 15e078ae0dd3..d49e358ea3e8 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -770,7 +770,7 @@ edx-name-affirmation==2.4.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -edx-opaque-keys[django]==2.10.0 +edx-opaque-keys[django]==2.11.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 defc09a23deb..8e06e72e60cc 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -564,7 +564,7 @@ edx-milestones==0.6.0 # via -r requirements/edx/base.txt edx-name-affirmation==2.4.0 # via -r requirements/edx/base.txt -edx-opaque-keys[django]==2.10.0 +edx-opaque-keys[django]==2.11.0 # via # -r requirements/edx/base.txt # edx-bulk-grades diff --git a/requirements/edx/paver.txt b/requirements/edx/paver.txt index faa0085f1631..d86acae05f4f 100644 --- a/requirements/edx/paver.txt +++ b/requirements/edx/paver.txt @@ -12,7 +12,7 @@ charset-normalizer==2.0.12 # requests dnspython==2.6.1 # via pymongo -edx-opaque-keys==2.10.0 +edx-opaque-keys==2.11.0 # via -r requirements/edx/paver.in idna==3.7 # via requests diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 49be07ecec71..548c0c636a6e 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -590,7 +590,7 @@ edx-milestones==0.6.0 # via -r requirements/edx/base.txt edx-name-affirmation==2.4.0 # via -r requirements/edx/base.txt -edx-opaque-keys[django]==2.10.0 +edx-opaque-keys[django]==2.11.0 # via # -r requirements/edx/base.txt # edx-bulk-grades