From ff8d3eae72001ea40b3dad0e8ef4b2b4c2ac158e Mon Sep 17 00:00:00 2001 From: ruzniaievdm Date: Wed, 10 Jan 2024 11:31:20 +0200 Subject: [PATCH 1/9] feat: Unit page API as DRF --- .../rest_api/v1/serializers/__init__.py | 3 +- .../rest_api/v1/serializers/home.py | 2 +- .../rest_api/v1/serializers/vertical_block.py | 92 +++++++++++ .../contentstore/rest_api/v1/urls.py | 7 + .../rest_api/v1/views/__init__.py | 1 + .../contentstore/rest_api/v1/views/home.py | 6 +- .../v1/views/tests/test_vertical_block.py | 67 ++++++++ .../rest_api/v1/views/vertical_block.py | 143 ++++++++++++++++++ cms/djangoapps/contentstore/utils.py | 124 +++++++++++++++ .../contentstore/views/component.py | 121 ++------------- .../djangoapps/content_staging/serializers.py | 5 +- 11 files changed, 459 insertions(+), 112 deletions(-) create mode 100644 cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py create mode 100644 cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py index 7e99a729ab..d5b41dd5b9 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/__init__.py @@ -6,7 +6,7 @@ from .course_rerun import CourseRerunSerializer from .course_team import CourseTeamSerializer from .course_index import CourseIndexSerializer from .grading import CourseGradingModelSerializer, CourseGradingSerializer -from .home import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer +from .home import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer from .proctoring import ( LimitedProctoredExamSettingsSerializer, ProctoredExamConfigurationSerializer, @@ -21,3 +21,4 @@ from .videos import ( VideoUsageSerializer, VideoDownloadSerializer ) +from .vertical_block import ContainerHandlerSerializer diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py index 80296b9a76..1afc51ed77 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/home.py @@ -31,7 +31,7 @@ class LibraryViewSerializer(serializers.Serializer): can_edit = serializers.BooleanField() -class CourseTabSerializer(serializers.Serializer): +class CourseHomeTabSerializer(serializers.Serializer): archived_courses = CourseCommonSerializer(required=False, many=True) courses = CourseCommonSerializer(required=False, many=True) in_process_course_actions = UnsucceededCourseSerializer(many=True, required=False, allow_null=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py new file mode 100644 index 0000000000..c5b54e200e --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/vertical_block.py @@ -0,0 +1,92 @@ +""" +API Serializers for unit page +""" + +from django.urls import reverse +from rest_framework import serializers + +from cms.djangoapps.contentstore.helpers import ( + xblock_studio_url, + xblock_type_display_name, +) + + +class ChildAncestorSerializer(serializers.Serializer): + """ + Serializer for representing child blocks in the ancestor XBlock. + """ + + url = serializers.SerializerMethodField() + display_name = serializers.CharField(source="display_name_with_default") + + def get_url(self, obj): + """ + Method to generate studio URL for the child block. + """ + return xblock_studio_url(obj) + + +class AncestorXBlockSerializer(serializers.Serializer): + """ + Serializer for representing the ancestor XBlock and its children. + """ + + children = ChildAncestorSerializer(many=True) + title = serializers.CharField() + is_last = serializers.BooleanField() + + +class ContainerXBlock(serializers.Serializer): + """ + Serializer for representing XBlock data. Doesn't include all data about XBlock. + """ + + display_name = serializers.CharField(source="display_name_with_default") + display_type = serializers.SerializerMethodField() + category = serializers.CharField() + + def get_display_type(self, obj): + """ + Method to get the display type name for the container XBlock. + """ + return xblock_type_display_name(obj) + + +class ContainerHandlerSerializer(serializers.Serializer): + """ + Serializer for container handler + """ + + language_code = serializers.CharField() + action = serializers.CharField() + xblock = ContainerXBlock() + is_unit_page = serializers.BooleanField() + is_collapsible = serializers.BooleanField() + position = serializers.IntegerField(min_value=1) + prev_url = serializers.CharField(allow_null=True) + next_url = serializers.CharField(allow_null=True) + new_unit_category = serializers.CharField() + outline_url = serializers.CharField() + ancestor_xblocks = AncestorXBlockSerializer(many=True) + component_templates = serializers.ListField(child=serializers.DictField()) + xblock_info = serializers.DictField() + draft_preview_link = serializers.CharField() + published_preview_link = serializers.CharField() + show_unit_tags = serializers.BooleanField() + user_clipboard = serializers.DictField() + is_fullwidth_content = serializers.BooleanField() + assets_url = serializers.SerializerMethodField() + unit_block_id = serializers.CharField(source="unit.location.block_id") + subsection_location = serializers.CharField(source="subsection.location") + + def get_assets_url(self, obj): + """ + Method to get the assets URL based on the course id. + """ + + context_course = obj.get("context_course", None) + if context_course: + return reverse( + "assets_handler", kwargs={"course_key_string": context_course.id} + ) + return None diff --git a/cms/djangoapps/contentstore/rest_api/v1/urls.py b/cms/djangoapps/contentstore/rest_api/v1/urls.py index 1af7cf46a6..66760ea3c3 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/urls.py +++ b/cms/djangoapps/contentstore/rest_api/v1/urls.py @@ -1,10 +1,12 @@ """ Contenstore API v1 URLs. """ +from django.conf import settings from django.urls import re_path, path from openedx.core.constants import COURSE_ID_PATTERN from .views import ( + ContainerHandlerView, CourseDetailsView, CourseTeamView, CourseIndexView, @@ -100,6 +102,11 @@ urlpatterns = [ CourseRerunView.as_view(), name="course_rerun" ), + re_path( + fr'^container_handler/{settings.USAGE_KEY_PATTERN}$', + ContainerHandlerView.as_view(), + name="container_handler" + ), # Authoring API # Do not use under v1 yet (Nov. 23). The Authoring API is still experimental and the v0 versions should be used diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py index 57d68ebd08..b7415b78c2 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/__init__.py @@ -15,3 +15,4 @@ from .videos import ( VideoDownloadView ) from .help_urls import HelpUrlsView +from .vertical_block import ContainerHandlerView diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/home.py b/cms/djangoapps/contentstore/rest_api/v1/views/home.py index f8ee907d2e..b06cec44b7 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/home.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/home.py @@ -8,7 +8,7 @@ from rest_framework.views import APIView from openedx.core.lib.api.view_utils import view_auth_classes from ....utils import get_home_context, get_course_context, get_library_context -from ..serializers import CourseHomeSerializer, CourseTabSerializer, LibraryTabSerializer +from ..serializers import CourseHomeSerializer, CourseHomeTabSerializer, LibraryTabSerializer @view_auth_classes(is_authenticated=True) @@ -102,7 +102,7 @@ class HomePageCoursesView(APIView): description="Query param to filter by course org", )], responses={ - 200: CourseTabSerializer, + 200: CourseHomeTabSerializer, 401: "The requester is not authenticated.", }, ) @@ -160,7 +160,7 @@ class HomePageCoursesView(APIView): "archived_courses": archived_courses, "in_process_course_actions": in_process_course_actions, } - serializer = CourseTabSerializer(courses_context) + serializer = CourseHomeTabSerializer(courses_context) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py new file mode 100644 index 0000000000..ff117c5ecf --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/tests/test_vertical_block.py @@ -0,0 +1,67 @@ +""" +Unit tests for the vertical block. +""" +from django.urls import reverse +from rest_framework import status + +from cms.djangoapps.contentstore.tests.utils import CourseTestCase +from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.tests.factories import BlockFactory # lint-amnesty, pylint: disable=wrong-import-order + + +class ContainerHandlerViewTest(CourseTestCase): + """ + Unit tests for the ContainerHandlerView. + """ + + def setUp(self): + super().setUp() + self.chapter = BlockFactory.create( + parent=self.course, category="chapter", display_name="Week 1" + ) + self.sequential = BlockFactory.create( + parent=self.chapter, category="sequential", display_name="Lesson 1" + ) + self.vertical = self._create_block(self.sequential, "vertical", "Unit") + + self.store = modulestore() + self.store.publish(self.vertical.location, self.user.id) + + def _get_reverse_url(self, location): + """ + Creates url to current handler view api + """ + return reverse( + "cms.djangoapps.contentstore:v1:container_handler", + kwargs={"usage_key_string": location}, + ) + + def _create_block(self, parent, category, display_name, **kwargs): + """ + Creates a block without publishing it. + """ + return BlockFactory.create( + parent=parent, + category=category, + display_name=display_name, + publish_item=False, + user_id=self.user.id, + **kwargs + ) + + def test_success_response(self): + """ + Check that endpoint is valid and success response. + """ + url = self._get_reverse_url(self.vertical.location) + response = self.client.get(url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_valid_usage_key_string(self): + """ + Check that invalid 'usage_key_string' raises Http404. + """ + usage_key_string = "i4x://InvalidOrg/InvalidCourse/vertical/static/InvalidContent" + url = self._get_reverse_url(usage_key_string) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py new file mode 100644 index 0000000000..3f9a048511 --- /dev/null +++ b/cms/djangoapps/contentstore/rest_api/v1/views/vertical_block.py @@ -0,0 +1,143 @@ +""" API Views for unit page """ + +import edx_api_doc_tools as apidocs +from django.http import Http404, HttpResponseBadRequest +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import UsageKey +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from cms.djangoapps.contentstore.utils import get_container_handler_context +from cms.djangoapps.contentstore.views.component import _get_item_in_course +from cms.djangoapps.contentstore.rest_api.v1.serializers import ContainerHandlerSerializer +from openedx.core.lib.api.view_utils import view_auth_classes +from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order + + +@view_auth_classes(is_authenticated=True) +class ContainerHandlerView(APIView): + """ + View for container xblock requests to get vertical data. + """ + + def get_object(self, usage_key_string): + """ + Get an object by usage-id of the block + """ + try: + usage_key = UsageKey.from_string(usage_key_string) + except InvalidKeyError: + raise Http404 # lint-amnesty, pylint: disable=raise-missing-from + return usage_key + + @apidocs.schema( + parameters=[ + apidocs.string_parameter( + "usage_key_string", + apidocs.ParameterLocation.PATH, + description="Container usage key", + ), + ], + responses={ + 200: ContainerHandlerSerializer, + 401: "The requester is not authenticated.", + 404: "The requested locator does not exist.", + }, + ) + def get(self, request: Request, usage_key_string: str): + """ + Get an object containing vertical data. + + **Example Request** + + GET /api/contentstore/v1/container_handler/{usage_key_string} + + **Response Values** + + If the request is successful, an HTTP 200 "OK" response is returned. + + The HTTP 200 response contains a single dict that contains keys that + are the vertical's container data. + + **Example Response** + + ```json + { + "language_code": "zh-cn", + "action": "view", + "xblock": { + "display_name": "Labs and Demos", + "display_type": "单元", + "category": "vertical" + }, + "is_unit_page": true, + "is_collapsible": false, + "position": 1, + "prev_url": "block-v1-edX%2BDemo_Course%2Btype%40vertical%2Bblock%404e592689563243c484", + "next_url": "block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40vertical%2Bblock%40vertical_aae927868e55", + "new_unit_category": "vertical", + "outline_url": "/course/course-v1:edX+DemoX+Demo_Course?format=concise", + "ancestor_xblocks": [ + { + "children": [ + { + "url": "/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%", + "display_name": "Introduction" + }, + ... + ], + "title": "Example Week 2: Get Interactive", + "is_last": false + }, + ... + ], + "component_templates": [ + { + "type": "advanced", + "templates": [ + { + "display_name": "批注", + "category": "annotatable", + "boilerplate_name": null, + "hinted": false, + "tab": "common", + "support_level": true + }, + ... + }, + ... + ], + "xblock_info": {}, + "draft_preview_link": "//preview.localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/...", + "published_preview_link": "///courses/course-v1:edX+DemoX+Demo_Course/jump_to/...", + "show_unit_tags": false, + "user_clipboard": { + "content": null, + "source_usage_key": "", + "source_context_title": "", + "source_edit_url": "" + }, + "is_fullwidth_content": false, + "assets_url": "/assets/course-v1:edX+DemoX+Demo_Course/", + "unit_block_id": "d6cee45205a449369d7ef8f159b22bdf", + "subsection_location": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@graded_simulations" + } + ``` + """ + usage_key = self.get_object(usage_key_string) + course_key = usage_key.course_key + with modulestore().bulk_operations(course_key): + try: + course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) + except ItemNotFoundError: + return HttpResponseBadRequest() + + context = get_container_handler_context(request, usage_key, course, xblock) + context.update({ + 'draft_preview_link': preview_lms_link, + 'published_preview_link': lms_link, + }) + serializer = ContainerHandlerSerializer(context) + return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index b2c3f59b9d..f74ef328fb 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -7,6 +7,7 @@ import logging from collections import defaultdict from contextlib import contextmanager from datetime import datetime, timezone +from urllib.parse import quote_plus from uuid import uuid4 from django.conf import settings @@ -1784,6 +1785,129 @@ def _get_course_index_context(request, course_key, course_block): return course_index_context +def get_container_handler_context(request, usage_key, course, xblock): # pylint: disable=too-many-statements + """ + Utils is used to get context for container xblock requests. + It is used for both DRF and django views. + """ + + from cms.djangoapps.contentstore.views.component import ( + get_component_templates, + get_unit_tags, + CONTAINER_TEMPLATES, + LIBRARY_BLOCK_TYPES, + ) + from cms.djangoapps.contentstore.helpers import get_parent_xblock, is_unit + from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import ( + add_container_page_publishing_info, + create_xblock_info, + ) + from openedx.core.djangoapps.content_staging import api as content_staging_api + + component_templates = get_component_templates(course) + ancestor_xblocks = [] + parent = get_parent_xblock(xblock) + action = request.GET.get('action', 'view') + + is_unit_page = is_unit(xblock) + unit = xblock if is_unit_page else None + + is_first = True + block = xblock + + # Build the breadcrumbs and find the ``Unit`` ancestor + # if it is not the immediate parent. + while parent: + + if unit is None and is_unit(block): + unit = block + + # add all to nav except current xblock page + if xblock != block: + current_block = { + 'title': block.display_name_with_default, + 'children': parent.get_children(), + 'is_last': is_first + } + is_first = False + ancestor_xblocks.append(current_block) + + block = parent + parent = get_parent_xblock(parent) + + ancestor_xblocks.reverse() + + if unit is None: + raise ValueError("Could not determine unit page") + + subsection = get_parent_xblock(unit) + if subsection is None: + raise ValueError(f"Could not determine parent subsection from unit {unit.location}") + + section = get_parent_xblock(subsection) + if section is None: + raise ValueError(f"Could not determine ancestor section from unit {unit.location}") + + # for the sequence navigator + prev_url, next_url = get_sibling_urls(subsection, unit.location) + # these are quoted here because they'll end up in a query string on the page, + # and quoting with mako will trigger the xss linter... + prev_url = quote_plus(prev_url) if prev_url else None + next_url = quote_plus(next_url) if next_url else None + + show_unit_tags = use_tagging_taxonomy_list_page() + unit_tags = None + if show_unit_tags and is_unit_page: + unit_tags = get_unit_tags(usage_key) + + # Fetch the XBlock info for use by the container page. Note that it includes information + # about the block's ancestors and siblings for use by the Unit Outline. + xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags) + + if is_unit_page: + add_container_page_publishing_info(xblock, xblock_info) + + # need to figure out where this item is in the list of children as the + # preview will need this + index = 1 + for child in subsection.get_children(): + if child.location == unit.location: + break + index += 1 + + # Get the status of the user's clipboard so they can paste components if they have something to paste + user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) + library_block_types = [problem_type['component'] for problem_type in LIBRARY_BLOCK_TYPES] + is_library_xblock = xblock.location.block_type in library_block_types + + context = { + 'language_code': request.LANGUAGE_CODE, + 'context_course': course, # Needed only for display of menus at top of page. + 'action': action, + 'xblock': xblock, + 'xblock_locator': xblock.location, + 'unit': unit, + 'is_unit_page': is_unit_page, + 'is_collapsible': is_library_xblock, + 'subsection': subsection, + 'section': section, + 'position': index, + 'prev_url': prev_url, + 'next_url': next_url, + 'new_unit_category': 'vertical', + 'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)), + 'ancestor_xblocks': ancestor_xblocks, + 'component_templates': component_templates, + 'xblock_info': xblock_info, + 'templates': CONTAINER_TEMPLATES, + 'show_unit_tags': show_unit_tags, + # Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API. + 'user_clipboard': user_clipboard, + 'is_fullwidth_content': is_library_xblock, + } + return context + + class StudioPermissionsService: """ Service that can provide information about a user's permissions. diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index bafcdad35c..ae0f62e331 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -4,7 +4,6 @@ Studio component views import logging -from urllib.parse import quote_plus from django.conf import settings from django.contrib.auth.decorators import login_required @@ -25,24 +24,14 @@ from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.student.auth import has_course_author_access from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag -from cms.djangoapps.contentstore.toggles import ( - use_new_problem_editor, - use_tagging_taxonomy_list_page, -) +from cms.djangoapps.contentstore.helpers import is_unit +from cms.djangoapps.contentstore.toggles import use_new_problem_editor, use_new_unit_page +from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration -from openedx.core.djangoapps.content_staging import api as content_staging_api from openedx.core.djangoapps.content_tagging.api import get_content_tags from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order -from ..toggles import use_new_unit_page -from ..utils import get_lms_link_for_item, get_sibling_urls, reverse_course_url, get_unit_url -from ..helpers import get_parent_xblock, is_unit, xblock_type_display_name -from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import ( - add_container_page_publishing_info, - create_xblock_info, - load_services_for_studio, -) __all__ = [ 'container_handler', @@ -121,6 +110,9 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st html: returns the HTML page for editing a container json: not currently supported """ + + from ..utils import get_container_handler_context, get_unit_url + if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): try: @@ -132,10 +124,6 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st course, xblock, lms_link, preview_lms_link = _get_item_in_course(request, usage_key) except ItemNotFoundError: return HttpResponseBadRequest() - component_templates = get_component_templates(course) - ancestor_xblocks = [] - parent = get_parent_xblock(xblock) - action = request.GET.get('action', 'view') is_unit_page = is_unit(xblock) unit = xblock if is_unit_page else None @@ -143,97 +131,12 @@ def container_handler(request, usage_key_string): # pylint: disable=too-many-st if is_unit_page and use_new_unit_page(course.id): return redirect(get_unit_url(course.id, unit.location)) - is_first = True - block = xblock - - # Build the breadcrumbs and find the ``Unit`` ancestor - # if it is not the immediate parent. - while parent: - - if unit is None and is_unit(block): - unit = block - - # add all to nav except current xblock page - if xblock != block: - current_block = { - 'title': block.display_name_with_default, - 'children': parent.get_children(), - 'is_last': is_first - } - is_first = False - ancestor_xblocks.append(current_block) - - block = parent - parent = get_parent_xblock(parent) - - ancestor_xblocks.reverse() - - assert unit is not None, "Could not determine unit page" - subsection = get_parent_xblock(unit) - assert subsection is not None, "Could not determine parent subsection from unit " + str( - unit.location) - section = get_parent_xblock(subsection) - assert section is not None, "Could not determine ancestor section from unit " + str(unit.location) - - # for the sequence navigator - prev_url, next_url = get_sibling_urls(subsection, unit.location) - # these are quoted here because they'll end up in a query string on the page, - # and quoting with mako will trigger the xss linter... - prev_url = quote_plus(prev_url) if prev_url else None - next_url = quote_plus(next_url) if next_url else None - - show_unit_tags = use_tagging_taxonomy_list_page() - unit_tags = None - if show_unit_tags and is_unit_page: - unit_tags = get_unit_tags(usage_key) - - # Fetch the XBlock info for use by the container page. Note that it includes information - # about the block's ancestors and siblings for use by the Unit Outline. - xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags) - - if is_unit_page: - add_container_page_publishing_info(xblock, xblock_info) - - # need to figure out where this item is in the list of children as the - # preview will need this - index = 1 - for child in subsection.get_children(): - if child.location == unit.location: - break - index += 1 - - # Get the status of the user's clipboard so they can paste components if they have something to paste - user_clipboard = content_staging_api.get_user_clipboard_json(request.user.id, request) - library_block_types = [problem_type['component'] for problem_type in LIBRARY_BLOCK_TYPES] - is_library_xblock = xblock.location.block_type in library_block_types - - return render_to_response('container.html', { - 'language_code': request.LANGUAGE_CODE, - 'context_course': course, # Needed only for display of menus at top of page. - 'action': action, - 'xblock': xblock, - 'xblock_locator': xblock.location, - 'unit': unit, - 'is_unit_page': is_unit_page, - 'is_collapsible': is_library_xblock, - 'subsection': subsection, - 'section': section, - 'position': index, - 'prev_url': prev_url, - 'next_url': next_url, - 'new_unit_category': 'vertical', - 'outline_url': '{url}?format=concise'.format(url=reverse_course_url('course_handler', course.id)), - 'ancestor_xblocks': ancestor_xblocks, - 'component_templates': component_templates, - 'xblock_info': xblock_info, + container_handler_context = get_container_handler_context(request, usage_key, course, xblock) + container_handler_context.update({ 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, - 'templates': CONTAINER_TEMPLATES, - 'show_unit_tags': show_unit_tags, - # Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API. - 'user_clipboard': user_clipboard, - 'is_fullwidth_content': is_library_xblock, }) + return render_to_response('container.html', container_handler_context) else: return HttpResponseBadRequest("Only supports HTML requests") @@ -242,6 +145,9 @@ def get_component_templates(courselike, library=False): # lint-amnesty, pylint: """ Returns the applicable component templates that can be used by the specified course or library. """ + + from ..helpers import xblock_type_display_name + def create_template_dict(name, category, support_level, boilerplate_name=None, tab="common", hinted=False): """ Creates a component template dict. @@ -545,6 +451,9 @@ def _get_item_in_course(request, usage_key): Verifies that the caller has permission to access this item. """ + + from ..utils import get_lms_link_for_item + # usage_key's course_key may have an empty run property usage_key = usage_key.replace(course_key=modulestore().fill_in_run(usage_key.course_key)) diff --git a/openedx/core/djangoapps/content_staging/serializers.py b/openedx/core/djangoapps/content_staging/serializers.py index 06b687838f..bf71fd9b1b 100644 --- a/openedx/core/djangoapps/content_staging/serializers.py +++ b/openedx/core/djangoapps/content_staging/serializers.py @@ -3,7 +3,6 @@ Serializers for the content libraries REST API """ from rest_framework import serializers -from cms.djangoapps.contentstore.helpers import xblock_studio_url, xblock_type_display_name from common.djangoapps.student.auth import has_studio_read_access from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError @@ -34,6 +33,8 @@ class StagedContentSerializer(serializers.ModelSerializer): def get_block_type_display(self, obj): """ Get the friendly name for this XBlock/component type """ + from cms.djangoapps.contentstore.helpers import xblock_type_display_name + return xblock_type_display_name(obj.block_type) @@ -50,6 +51,8 @@ class UserClipboardSerializer(serializers.Serializer): def get_source_edit_url(self, obj) -> str: """ Get the URL where the user can edit the given XBlock, if it exists """ + from cms.djangoapps.contentstore.helpers import xblock_studio_url + request = self.context.get("request", None) user = request.user if request else None if not user: From 9db7e57b9b2c467a97a950f39168624cfd84888d Mon Sep 17 00:00:00 2001 From: saleem-latif Date: Tue, 30 Jan 2024 07:51:29 +0000 Subject: [PATCH 2/9] feat: Upgrade Python dependency edx-enterprise Commit generated by workflow `openedx/edx-platform/.github/workflows/upgrade-one-python-dependency.yml@refs/heads/master` --- 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 7e3e48f88b..dd88bbbd98 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -23,7 +23,7 @@ click>=8.0,<9.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.10.11 +edx-enterprise==4.11.0 # django-oauth-toolkit version >=2.0.0 has breaking changes. More details # mentioned on this issue https://github.com/openedx/edx-platform/issues/32884 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 6d5a04100d..6e31fc148c 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -475,7 +475,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.10.11 +edx-enterprise==4.11.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index f04ae1ec5f..eb38b3778f 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -755,7 +755,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.10.11 +edx-enterprise==4.11.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 60684bc3fd..fe0338ca4a 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -553,7 +553,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.10.11 +edx-enterprise==4.11.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index f597a2b442..7c038d3637 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -579,7 +579,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.10.11 +edx-enterprise==4.11.0 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt From 0b7e27390c1769adaa09c6b1a05f6c6a8d2104bf Mon Sep 17 00:00:00 2001 From: IrfanUddinAhmad Date: Sun, 28 Jan 2024 11:34:24 +0500 Subject: [PATCH 3/9] feat: Added XblockMixin for skill tagging --- lms/envs/common.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lms/envs/common.py b/lms/envs/common.py index b567b20a37..7ed5b76f62 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -68,6 +68,10 @@ from openedx.core.djangoapps.theming.helpers_dirs import ( from openedx.core.lib.derived import derived, derived_collection_entry from openedx.core.release import doc_version from lms.djangoapps.lms_xblock.mixin import LmsBlockMixin +try: + from skill_tagging.skill_tagging_mixin import SkillTaggingMixin +except ImportError: + SkillTaggingMixin = None ################################### FEATURES ################################### # .. setting_name: PLATFORM_NAME @@ -1633,6 +1637,8 @@ from xmodule.x_module import XModuleMixin # lint-amnesty, pylint: disable=wrong # This should be moved into an XBlock Runtime/Application object # once the responsibility of XBlock creation is moved out of modulestore - cpennington XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin, EditInfoMixin) +if SkillTaggingMixin: + XBLOCK_MIXINS += (SkillTaggingMixin,) XBLOCK_EXTRA_MIXINS = () # .. setting_name: XBLOCK_FIELD_DATA_WRAPPERS From cc8f83792c529935ee3e8846cca901b07cdd27a8 Mon Sep 17 00:00:00 2001 From: edX requirements bot <49161187+edx-requirements-bot@users.noreply.github.com> Date: Tue, 30 Jan 2024 07:44:28 -0500 Subject: [PATCH 4/9] chore: Updating Python Requirements (#34140) --- requirements/edx/base.txt | 12 ++++++------ requirements/edx/coverage.txt | 2 +- requirements/edx/development.txt | 28 ++++++++++++++-------------- requirements/edx/doc.txt | 12 ++++++------ requirements/edx/semgrep.txt | 2 +- requirements/edx/testing.txt | 26 +++++++++++++------------- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 6e31fc148c..ba85aded42 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -8,7 +8,7 @@ # via -r requirements/edx/github.in acid-xblock==0.2.1 # via -r requirements/edx/kernel.in -aiohttp==3.9.1 +aiohttp==3.9.3 # via # geoip2 # openai @@ -73,13 +73,13 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/kernel.in -boto3==1.34.28 +boto3==1.34.30 # via # -r requirements/edx/kernel.in # django-ses # fs-s3fs # ora2 -botocore==1.34.28 +botocore==1.34.30 # via # -r requirements/edx/kernel.in # boto3 @@ -419,7 +419,7 @@ edx-auth-backends==4.2.0 # via # -r requirements/edx/kernel.in # openedx-blockstore -edx-braze-client==0.2.1 +edx-braze-client==0.2.2 # via # -r requirements/edx/bundled.in # edx-enterprise @@ -936,7 +936,7 @@ python3-openid==3.2.0 ; python_version >= "3" # social-auth-core python3-saml==1.16.0 # via -r requirements/edx/kernel.in -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/kernel.in # babel @@ -976,7 +976,7 @@ redis==5.0.1 # via # -r requirements/edx/kernel.in # walrus -referencing==0.32.1 +referencing==0.33.0 # via # jsonschema # jsonschema-specifications diff --git a/requirements/edx/coverage.txt b/requirements/edx/coverage.txt index 3d8191aa4c..040832be39 100644 --- a/requirements/edx/coverage.txt +++ b/requirements/edx/coverage.txt @@ -6,7 +6,7 @@ # chardet==5.2.0 # via diff-cover -coverage==7.4.0 +coverage==7.4.1 # via -r requirements/edx/coverage.in diff-cover==8.0.3 # via -r requirements/edx/coverage.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index eb38b3778f..09bbb04832 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -16,7 +16,7 @@ acid-xblock==0.2.1 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -aiohttp==3.9.1 +aiohttp==3.9.3 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -144,14 +144,14 @@ boto==2.49.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -boto3==1.34.28 +boto3==1.34.30 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.28 +botocore==1.34.30 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -280,7 +280,7 @@ coreschema==0.0.4 # -r requirements/edx/testing.txt # coreapi # drf-yasg -coverage[toml]==7.4.0 +coverage[toml]==7.4.1 # via # -r requirements/edx/testing.txt # coverage @@ -333,7 +333,7 @@ deprecated==1.2.14 # jwcrypto diff-cover==8.0.3 # via -r requirements/edx/testing.txt -dill==0.3.7 +dill==0.3.8 # via # -r requirements/edx/testing.txt # pylint @@ -686,7 +686,7 @@ edx-auth-backends==4.2.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # openedx-blockstore -edx-braze-client==0.2.1 +edx-braze-client==0.2.2 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -896,7 +896,7 @@ execnet==2.0.2 # pytest-xdist factory-boy==3.3.0 # via -r requirements/edx/testing.txt -faker==22.5.1 +faker==22.6.0 # via # -r requirements/edx/testing.txt # factory-boy @@ -1463,11 +1463,11 @@ pycryptodomex==3.20.0 # edx-proctoring # lti-consumer-xblock # pyjwkest -pydantic==2.5.3 +pydantic==2.6.0 # via # -r requirements/edx/testing.txt # fastapi -pydantic-core==2.14.6 +pydantic-core==2.16.1 # via # -r requirements/edx/testing.txt # pydantic @@ -1592,7 +1592,7 @@ pysrt==1.1.2 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # edxval -pytest==7.4.4 +pytest==8.0.0 # via # -r requirements/edx/testing.txt # pylint-pytest @@ -1665,7 +1665,7 @@ python3-saml==1.16.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -1717,7 +1717,7 @@ redis==5.0.1 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # walrus -referencing==0.32.1 +referencing==0.33.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt @@ -2038,7 +2038,7 @@ tqdm==4.66.1 # -r requirements/edx/testing.txt # nltk # openai -types-pytz==2023.3.1.1 +types-pytz==2023.4.0.20240130 # via django-stubs types-pyyaml==6.0.12.12 # via @@ -2110,7 +2110,7 @@ user-util==1.0.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -uvicorn==0.27.0 +uvicorn==0.27.0.post1 # via # -r requirements/edx/testing.txt # pact-python diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index fe0338ca4a..f3a48ce464 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -10,7 +10,7 @@ accessible-pygments==0.0.4 # via pydata-sphinx-theme acid-xblock==0.2.1 # via -r requirements/edx/base.txt -aiohttp==3.9.1 +aiohttp==3.9.3 # via # -r requirements/edx/base.txt # geoip2 @@ -102,13 +102,13 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/base.txt -boto3==1.34.28 +boto3==1.34.30 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.28 +botocore==1.34.30 # via # -r requirements/edx/base.txt # boto3 @@ -497,7 +497,7 @@ edx-auth-backends==4.2.0 # via # -r requirements/edx/base.txt # openedx-blockstore -edx-braze-client==0.2.1 +edx-braze-client==0.2.2 # via # -r requirements/edx/base.txt # edx-enterprise @@ -1119,7 +1119,7 @@ python3-openid==3.2.0 ; python_version >= "3" # social-auth-core python3-saml==1.16.0 # via -r requirements/edx/base.txt -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/base.txt # babel @@ -1160,7 +1160,7 @@ redis==5.0.1 # via # -r requirements/edx/base.txt # walrus -referencing==0.32.1 +referencing==0.33.0 # via # -r requirements/edx/base.txt # jsonschema diff --git a/requirements/edx/semgrep.txt b/requirements/edx/semgrep.txt index 11bfb4b51d..00735298e4 100644 --- a/requirements/edx/semgrep.txt +++ b/requirements/edx/semgrep.txt @@ -60,7 +60,7 @@ pkgutil-resolve-name==1.3.10 # via jsonschema pygments==2.17.2 # via rich -referencing==0.32.1 +referencing==0.33.0 # via # jsonschema # jsonschema-specifications diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 7c038d3637..f83a39fc21 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -8,7 +8,7 @@ # via -r requirements/edx/base.txt acid-xblock==0.2.1 # via -r requirements/edx/base.txt -aiohttp==3.9.1 +aiohttp==3.9.3 # via # -r requirements/edx/base.txt # geoip2 @@ -104,13 +104,13 @@ bleach[css]==6.1.0 # xblock-poll boto==2.49.0 # via -r requirements/edx/base.txt -boto3==1.34.28 +boto3==1.34.30 # via # -r requirements/edx/base.txt # django-ses # fs-s3fs # ora2 -botocore==1.34.28 +botocore==1.34.30 # via # -r requirements/edx/base.txt # boto3 @@ -208,7 +208,7 @@ coreschema==0.0.4 # -r requirements/edx/base.txt # coreapi # drf-yasg -coverage[toml]==7.4.0 +coverage[toml]==7.4.1 # via # -r requirements/edx/coverage.txt # pytest-cov @@ -250,7 +250,7 @@ deprecated==1.2.14 # jwcrypto diff-cover==8.0.3 # via -r requirements/edx/coverage.txt -dill==0.3.7 +dill==0.3.8 # via pylint distlib==0.3.8 # via virtualenv @@ -523,7 +523,7 @@ edx-auth-backends==4.2.0 # via # -r requirements/edx/base.txt # openedx-blockstore -edx-braze-client==0.2.1 +edx-braze-client==0.2.2 # via # -r requirements/edx/base.txt # edx-enterprise @@ -685,7 +685,7 @@ execnet==2.0.2 # via pytest-xdist factory-boy==3.3.0 # via -r requirements/edx/testing.in -faker==22.5.1 +faker==22.6.0 # via factory-boy fastapi==0.109.0 # via pact-python @@ -1095,9 +1095,9 @@ pycryptodomex==3.20.0 # edx-proctoring # lti-consumer-xblock # pyjwkest -pydantic==2.5.3 +pydantic==2.6.0 # via fastapi -pydantic-core==2.14.6 +pydantic-core==2.16.1 # via pydantic pygments==2.17.2 # via @@ -1185,7 +1185,7 @@ pysrt==1.1.2 # via # -r requirements/edx/base.txt # edxval -pytest==7.4.4 +pytest==8.0.0 # via # -r requirements/edx/testing.in # pylint-pytest @@ -1247,7 +1247,7 @@ python3-openid==3.2.0 ; python_version >= "3" # social-auth-core python3-saml==1.16.0 # via -r requirements/edx/base.txt -pytz==2023.3.post1 +pytz==2023.4 # via # -r requirements/edx/base.txt # babel @@ -1287,7 +1287,7 @@ redis==5.0.1 # via # -r requirements/edx/base.txt # walrus -referencing==0.32.1 +referencing==0.33.0 # via # -r requirements/edx/base.txt # jsonschema @@ -1546,7 +1546,7 @@ urllib3==1.26.18 # snowflake-connector-python user-util==1.0.0 # via -r requirements/edx/base.txt -uvicorn==0.27.0 +uvicorn==0.27.0.post1 # via pact-python vine==5.1.0 # via From 76330b36b004f1636908ada962bbc75f4857eeb3 Mon Sep 17 00:00:00 2001 From: Asespinel <79876430+Asespinel@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:29:09 -0500 Subject: [PATCH 5/9] feat: added setting to disable the survey report banner entirely (#34092) * feat: added setting to disable the survey report banner entirely * fix: fixed unit test with new setting * refactor: changed conditions for better code readability * feat: added exception to stop the report from generating if the setting is set to false * chore: updated the readme file to include the new setting * refactor: move survey settings to common and disable admin by setting * docs: typos in README Co-authored-by: Tim McCormack * refactor: set default values to survey report settings * refactor: rename ENABLE_SURVEY_REPORT setting to SURVEY_REPORT_ENABLE * test: fix quality tests --------- Co-authored-by: Alejandro Cardenas Co-authored-by: Tim McCormack --- lms/envs/common.py | 28 ++++++++ lms/envs/production.py | 8 --- lms/envs/test.py | 1 + openedx/features/survey_report/README.rst | 3 +- openedx/features/survey_report/admin.py | 5 +- openedx/features/survey_report/api.py | 2 + .../survey_report/context_processors.py | 68 +++++++++++++------ 7 files changed, 86 insertions(+), 29 deletions(-) diff --git a/lms/envs/common.py b/lms/envs/common.py index 7ed5b76f62..3da23a4ad8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5478,3 +5478,31 @@ derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.cert derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.certificate.revoked.v1', 'learning-certificate-lifecycle', 'enabled') BEAMER_PRODUCT_ID = "" + +#### Survey Report #### +# .. toggle_name: SURVEY_REPORT_ENABLE +# .. toggle_implementation: DjangoSetting +# .. toggle_default: True +# .. toggle_description: Set to True to enable the feature to generate and send survey reports. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2024-01-30 +SURVEY_REPORT_ENABLE = True +# .. setting_name: SURVEY_REPORT_ENDPOINT +# .. setting_default: Open edX organization endpoint +# .. setting_description: Endpoint where the report will be sent. +SURVEY_REPORT_ENDPOINT = 'https://hooks.zapier.com/hooks/catch/11595998/3ouwv7m/' +# .. toggle_name: ANONYMOUS_SURVEY_REPORT +# .. toggle_implementation: DjangoSetting +# .. toggle_default: False +# .. toggle_description: If enable, the survey report will be send a UUID as ID instead of use lms site name. +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2023-02-21 +ANONYMOUS_SURVEY_REPORT = False +# .. setting_name: SURVEY_REPORT_CHECK_THRESHOLD +# .. setting_default: every 6 months +# .. setting_description: Survey report banner will appear if a survey report is not sent in the months defined. +SURVEY_REPORT_CHECK_THRESHOLD = 6 +# .. setting_name: SURVEY_REPORT_EXTRA_DATA +# .. setting_default: empty dictionary +# .. setting_description: Dictionary with additional information that you want to share in the report. +SURVEY_REPORT_EXTRA_DATA = {} diff --git a/lms/envs/production.py b/lms/envs/production.py index 3d58a3bde8..d56a5631bb 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1125,14 +1125,6 @@ COURSE_LIVE_GLOBAL_CREDENTIALS["BIG_BLUE_BUTTON"] = { "URL": ENV_TOKENS.get('BIG_BLUE_BUTTON_GLOBAL_URL', None), } -############## Settings for survey report ############## -SURVEY_REPORT_EXTRA_DATA = ENV_TOKENS.get('SURVEY_REPORT_EXTRA_DATA', {}) -SURVEY_REPORT_ENDPOINT = ENV_TOKENS.get('SURVEY_REPORT_ENDPOINT', - 'https://hooks.zapier.com/hooks/catch/11595998/3ouwv7m/') -ANONYMOUS_SURVEY_REPORT = ENV_TOKENS.get('ANONYMOUS_SURVEY_REPORT', False) - -SURVEY_REPORT_CHECK_THRESHOLD = ENV_TOKENS.get('SURVEY_REPORT_CHECK_THRESHOLD', 6) - AVAILABLE_DISCUSSION_TOURS = ENV_TOKENS.get('AVAILABLE_DISCUSSION_TOURS', []) ############## NOTIFICATIONS EXPIRY ############## diff --git a/lms/envs/test.py b/lms/envs/test.py index 6d87a05848..14c10e52d3 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -664,6 +664,7 @@ MFE_CONFIG_OVERRIDES = { SURVEY_REPORT_EXTRA_DATA = {} SURVEY_REPORT_ENDPOINT = "https://example.com/survey_report" SURVEY_REPORT_CHECK_THRESHOLD = 6 +SURVEY_REPORT_ENABLE = True ANONYMOUS_SURVEY_REPORT = False ######################## Subscriptions API SETTINGS ######################## diff --git a/openedx/features/survey_report/README.rst b/openedx/features/survey_report/README.rst index b5250947f3..412e319f22 100644 --- a/openedx/features/survey_report/README.rst +++ b/openedx/features/survey_report/README.rst @@ -65,6 +65,7 @@ You have the following settings to customize the behavior of your reports. - ``ANONYMOUS_SURVEY_REPORT``: This is a boolean to specify if you want to use your LMS domain as ID for your report or to send the information anonymously with a UUID. By default, this setting is False. +- ``SURVEY_REPORT_ENABLE``: This is a boolean to specify if you want to enable or disable the survey report feature completely. The banner will disappear and the report generation will be disabled if set to False. By default, this setting is True. About the Survey Report Admin Banner ------------------------------------- @@ -74,4 +75,4 @@ This app implements a banner to make it easy for the Open edX operators to gener .. image:: docs/_images/survey_report_banner.png :alt: Survey Report Banner -**Note:** The banner will appear if a survey report is not sent in the months defined in the ``context_processor`` file, by default, is set to appear monthly. +**Note:** The banner will appear if a survey report is not sent in the months defined in the ``context_processor`` file, by default, is set to appear every 6 months. diff --git a/openedx/features/survey_report/admin.py b/openedx/features/survey_report/admin.py index f2a422de2e..adb759a84f 100644 --- a/openedx/features/survey_report/admin.py +++ b/openedx/features/survey_report/admin.py @@ -4,6 +4,7 @@ Django Admin page for SurveyReport. from django.contrib import admin +from django.conf import settings from .models import SurveyReport from .api import send_report_to_external_api @@ -80,4 +81,6 @@ class SurveyReportAdmin(admin.ModelAdmin): del actions['delete_selected'] return actions -admin.site.register(SurveyReport, SurveyReportAdmin) + +if settings.SURVEY_REPORT_ENABLE: + admin.site.register(SurveyReport, SurveyReportAdmin) diff --git a/openedx/features/survey_report/api.py b/openedx/features/survey_report/api.py index bb46959337..cb8eadc29f 100644 --- a/openedx/features/survey_report/api.py +++ b/openedx/features/survey_report/api.py @@ -45,6 +45,8 @@ def get_report_data() -> dict: def generate_report() -> None: """ Generate a report with relevant data.""" + if not settings.SURVEY_REPORT_ENABLE: + raise Exception("Survey report generation is not enabled") data = {} survey_report = SurveyReport(**data) survey_report.save() diff --git a/openedx/features/survey_report/context_processors.py b/openedx/features/survey_report/context_processors.py index 967291d969..f2e8892d84 100644 --- a/openedx/features/survey_report/context_processors.py +++ b/openedx/features/survey_report/context_processors.py @@ -1,34 +1,64 @@ """ -This is the survey report contex_processor modules +This module provides context processors for integrating survey report functionality +into Django admin sites. -This is meant to determine the visibility of the survey report banner -across all admin pages in case a survey report has not been generated +It includes functions for determining whether to display a survey report banner and +calculating the date threshold for displaying the banner. +Functions: +- admin_extra_context(request): +Sends extra context to every admin site, determining whether to display the +survey report banner based on defined settings and conditions. + +- should_show_survey_report_banner(): +Determines whether to show the survey report banner based on the threshold. + +- get_months_threshold(months): +Calculates the date threshold based on the specified number of months. + +Dependencies: +- Django: settings, reverse, shortcuts +- datetime: datetime +- dateutil.relativedelta: relativedelta + +Usage: +This module is designed to be imported into Django projects with admin functionality. +It enhances the admin interface by providing dynamic context for displaying a survey +report banner based on defined conditions and settings. """ - -from datetime import datetime -from dateutil.relativedelta import relativedelta # for months test -from .models import SurveyReport -from django.urls import reverse from django.conf import settings +from django.urls import reverse +from datetime import datetime +from dateutil.relativedelta import relativedelta +from .models import SurveyReport def admin_extra_context(request): """ - This function sends extra context to every admin site - - The current treshhold to show the banner is one month but this can be redefined in the future - + This function sends extra context to every admin site. + The current threshold to show the banner is one month but this can be redefined in the future. """ - months = settings.SURVEY_REPORT_CHECK_THRESHOLD - if not request.path.startswith(reverse('admin:index')): - return {'show_survey_report_banner': False, } + if not settings.SURVEY_REPORT_ENABLE or not request.path.startswith(reverse('admin:index')): + return {'show_survey_report_banner': False} + + return {'show_survey_report_banner': should_show_survey_report_banner()} + + +def should_show_survey_report_banner(): + """ + Determine whether to show the survey report banner based on the threshold. + """ + months_threshold = get_months_threshold(settings.SURVEY_REPORT_CHECK_THRESHOLD) try: latest_report = SurveyReport.objects.latest('created_at') - months_treshhold = datetime.today().date() - relativedelta(months=months) # Calculate date one month ago - show_survey_report_banner = latest_report.created_at.date() <= months_treshhold + return latest_report.created_at.date() <= months_threshold except SurveyReport.DoesNotExist: - show_survey_report_banner = True + return True - return {'show_survey_report_banner': show_survey_report_banner, } + +def get_months_threshold(months): + """ + Calculate the date threshold based on the specified number of months. + """ + return datetime.today().date() - relativedelta(months=months) From 3bf3aeaf314a4d1a9b10c7384860cc81c370611b Mon Sep 17 00:00:00 2001 From: Alejandro Cardenas Date: Tue, 30 Jan 2024 15:13:54 -0500 Subject: [PATCH 6/9] refactor: change survey report message location (#34126) * refactor: change survey report message location --- lms/templates/admin/base_site.html | 4 +- .../templates/survey_report/admin_banner.html | 55 +------------------ 2 files changed, 3 insertions(+), 56 deletions(-) diff --git a/lms/templates/admin/base_site.html b/lms/templates/admin/base_site.html index fa93f9c3e8..2dfd0bef7a 100644 --- a/lms/templates/admin/base_site.html +++ b/lms/templates/admin/base_site.html @@ -21,6 +21,6 @@ {% endblock %} -{% block header %}{{ block.super }} +{% block messages %}{{ block.super }} {% include "survey_report/admin_banner.html" %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/openedx/features/survey_report/templates/survey_report/admin_banner.html b/openedx/features/survey_report/templates/survey_report/admin_banner.html index 6a8e2ea92e..0e8034380e 100644 --- a/openedx/features/survey_report/templates/survey_report/admin_banner.html +++ b/openedx/features/survey_report/templates/survey_report/admin_banner.html @@ -11,64 +11,11 @@

If you agree and want to send a report you can click the button below. You can always send reports and see the status of reports you have sent in the past at admin/survey_report/surveyreport/ .

- -
+ {% csrf_token %}
- {% endif %} - - - - {% endblock %} From 4b7ef2697e84bae269983dfce63f1af60bb247df Mon Sep 17 00:00:00 2001 From: Alejandro Cardenas Date: Tue, 30 Jan 2024 15:14:14 -0500 Subject: [PATCH 7/9] feat: add state sent to state column (#34127) --- openedx/features/survey_report/admin.py | 14 +++++++++++++- .../templates/survey_report/change_list.html | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/openedx/features/survey_report/admin.py b/openedx/features/survey_report/admin.py index adb759a84f..a5719d966a 100644 --- a/openedx/features/survey_report/admin.py +++ b/openedx/features/survey_report/admin.py @@ -22,7 +22,7 @@ class SurveyReportAdmin(admin.ModelAdmin): ) list_display = ( - 'id', 'summary', 'created_at', 'state' + 'id', 'summary', 'created_at', 'report_state' ) actions = ['send_report'] @@ -81,6 +81,18 @@ class SurveyReportAdmin(admin.ModelAdmin): del actions['delete_selected'] return actions + def report_state(self, obj): + """ + Method to define the custom State column with the new "send" state, + to avoid modifying the current models. + """ + try: + if obj.surveyreportupload_set.last().is_uploaded(): + return "Sent" + except AttributeError: + return obj.state.capitalize() + report_state.short_description = 'State' + if settings.SURVEY_REPORT_ENABLE: admin.site.register(SurveyReport, SurveyReportAdmin) diff --git a/openedx/features/survey_report/templates/survey_report/change_list.html b/openedx/features/survey_report/templates/survey_report/change_list.html index c4c0ebcb67..2cd947273e 100644 --- a/openedx/features/survey_report/templates/survey_report/change_list.html +++ b/openedx/features/survey_report/templates/survey_report/change_list.html @@ -5,7 +5,7 @@
  • {% csrf_token %} - +
  • From d0a49d1a01bda06722139513919b1aafcf739fa9 Mon Sep 17 00:00:00 2001 From: Kaustav Banerjee Date: Wed, 31 Jan 2024 04:18:34 +0530 Subject: [PATCH 8/9] fix: add missing function import in certificate template (#33904) * fix: add missing function import in certificate template * test: add test case to check certificates generated when GA4 is enabled --- lms/djangoapps/certificates/tests/test_views.py | 16 ++++++++++++++++ .../certificates/accomplishment-base.html | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index 32db30d12f..520baa2a8f 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -165,3 +165,19 @@ class CertificatesViewsSiteTests(ModuleStoreTestCase): response, 'This should not survive being overwritten by static content', ) + + @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED, GOOGLE_ANALYTICS_4_ID='GA-abc') + @with_site_configuration(configuration={'platform_name': 'My Platform Site'}) + def test_html_view_with_g4(self): + test_url = get_certificate_url( + user_id=self.user.id, + course_id=str(self.course.id), + uuid=self.cert.verify_uuid + ) + self._add_course_certificates(count=1, signatory_count=2) + response = self.client.get(test_url) + self.assertContains( + response, + 'awarded this My Platform Site Honor Code Certificate of Completion', + ) + self.assertContains(response, 'googletagmanager') diff --git a/lms/templates/certificates/accomplishment-base.html b/lms/templates/certificates/accomplishment-base.html index e48cd50354..c44baff1c2 100644 --- a/lms/templates/certificates/accomplishment-base.html +++ b/lms/templates/certificates/accomplishment-base.html @@ -1,7 +1,9 @@ <%page expression_filter="h"/> <%namespace name='static' file='/static_content.html'/> -<%! from django.utils.translation import gettext as _%> - +<%! +from django.utils.translation import gettext as _ +from openedx.core.djangolib.js_utils import js_escaped_string +%> <% # set doc language direction from django.utils.translation import get_language_bidi From a8206c1251904c9a81a0c511d69cda7f16493391 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:25:56 +0500 Subject: [PATCH 9/9] feat: Upgrade Python dependency edx-enterprise (#34144) Added management command to fix `LearnerDataTransmissionAudit` table records. Commit generated by workflow `openedx/edx-platform/.github/workflows/upgrade-one-python-dependency.yml@refs/heads/master` Co-authored-by: zamanafzal Co-authored-by: Zaman Afzal --- 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 dd88bbbd98..fb6a7078d0 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -23,7 +23,7 @@ click>=8.0,<9.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.11.0 +edx-enterprise==4.11.1 # django-oauth-toolkit version >=2.0.0 has breaking changes. More details # mentioned on this issue https://github.com/openedx/edx-platform/issues/32884 diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index ba85aded42..5a664f8003 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -475,7 +475,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.11.0 +edx-enterprise==4.11.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/kernel.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 09bbb04832..a9c9a55f6c 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -755,7 +755,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.11.0 +edx-enterprise==4.11.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/doc.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index f3a48ce464..57a0356b3a 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -553,7 +553,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.11.0 +edx-enterprise==4.11.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index f83a39fc21..17bfd0cf07 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -579,7 +579,7 @@ edx-drf-extensions==10.1.0 # edx-when # edxval # openedx-learning -edx-enterprise==4.11.0 +edx-enterprise==4.11.1 # via # -c requirements/edx/../constraints.txt # -r requirements/edx/base.txt