From c0b6260c3e8cd6bf452bedf9eaa060c01ff2699f Mon Sep 17 00:00:00 2001 From: Nizar Mahmoud <nizarmah@hotmail.com> Date: Thu, 6 Aug 2020 20:55:08 +0300 Subject: [PATCH] Adds Custom Extra Fields to Course Blocks API through LMS Settings --- .../course_api/blocks/serializers.py | 5 +- .../course_api/blocks/tests/test_views.py | 59 ++++++++++++++++ .../blocks/transformers/blocks_api.py | 4 ++ .../blocks/transformers/extra_fields.py | 69 +++++++++++++++++++ .../transformers/tests/test_extra_fields.py | 61 ++++++++++++++++ lms/djangoapps/course_api/blocks/views.py | 4 ++ lms/envs/common.py | 5 ++ lms/envs/test.py | 5 ++ 8 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/course_api/blocks/transformers/extra_fields.py create mode 100644 lms/djangoapps/course_api/blocks/transformers/tests/test_extra_fields.py diff --git a/lms/djangoapps/course_api/blocks/serializers.py b/lms/djangoapps/course_api/blocks/serializers.py index eee1d5d4ed7..d8361fa81f6 100644 --- a/lms/djangoapps/course_api/blocks/serializers.py +++ b/lms/djangoapps/course_api/blocks/serializers.py @@ -15,6 +15,7 @@ from .transformers.block_counts import BlockCountsTransformer from .transformers.milestones import MilestonesAndSpecialExamsTransformer from .transformers.navigation import BlockNavigationTransformer from .transformers.student_view import StudentViewTransformer +from .transformers.extra_fields import ExtraFieldsTransformer class SupportedFieldType(object): @@ -80,7 +81,9 @@ SUPPORTED_FIELDS = [ BlockCompletionTransformer.COMPLETION, BlockCompletionTransformer, 'completion' - ) + ), + + *[SupportedFieldType(field_name) for field_name in ExtraFieldsTransformer.get_requested_extra_fields()], ] # This lists the names of all fields that are allowed diff --git a/lms/djangoapps/course_api/blocks/tests/test_views.py b/lms/djangoapps/course_api/blocks/tests/test_views.py index 99061b2cd7f..811cbc87c90 100644 --- a/lms/djangoapps/course_api/blocks/tests/test_views.py +++ b/lms/djangoapps/course_api/blocks/tests/test_views.py @@ -222,6 +222,65 @@ class TestBlocksView(SharedModuleStoreTestCase): block_data['type'] in self.BLOCK_TYPES_WITH_STUDENT_VIEW_DATA ) + def test_extra_field_when_requested(self): + """ + Tests if all requested extra fields appear in output + + Requests the fields specified under "COURSE_BLOCKS_API_EXTRA_FIELDS" + in the Test Django settings + + Test setting "COURSE_BLOCKS_API_EXTRA_FIELDS" contains: + - other_course_settings + - course_visibility + """ + response = self.verify_response(params={ + 'all_blocks': True, + 'requested_fields': ['other_course_settings', 'course_visibility'], + }) + self.verify_response_block_dict(response) + for block_data in six.itervalues(response.data['blocks']): + self.assert_in_iff( + 'other_course_settings', + block_data, + block_data['type'] == 'course' + ) + + self.assert_in_iff( + 'course_visibility', + block_data, + block_data['type'] == 'course' + ) + + def test_extra_field_when_not_requested(self): + """ + Tests if fields that weren't requested would appear in output + + Requests some of the fields specified under + "COURSE_BLOCKS_API_EXTRA_FIELDS" in the Test Django settings + The other extra fields specified in Test Django settings weren't + requested to see if they would show up in the output or not + + Test setting "COURSE_BLOCKS_API_EXTRA_FIELDS" contains: + - other_course_settings + - course_visibility + """ + response = self.verify_response(params={ + 'all_blocks': True, + 'requested_fields': ['course_visibility'], + }) + self.verify_response_block_dict(response) + for block_data in six.itervalues(response.data['blocks']): + self.assertNotIn( + 'other_course_settings', + block_data + ) + + self.assert_in_iff( + 'course_visibility', + block_data, + block_data['type'] == 'course' + ) + def test_navigation_param(self): response = self.verify_response(params={'nav_depth': 10}) self.verify_response_block_dict(response) diff --git a/lms/djangoapps/course_api/blocks/transformers/blocks_api.py b/lms/djangoapps/course_api/blocks/transformers/blocks_api.py index 5e42f277c34..bd733a9cfb3 100644 --- a/lms/djangoapps/course_api/blocks/transformers/blocks_api.py +++ b/lms/djangoapps/course_api/blocks/transformers/blocks_api.py @@ -10,6 +10,7 @@ from .block_depth import BlockDepthTransformer from .navigation import BlockNavigationTransformer from .student_view import StudentViewTransformer from .video_urls import VideoBlockURLTransformer +from .extra_fields import ExtraFieldsTransformer class BlocksAPITransformer(BlockStructureTransformer): @@ -22,6 +23,7 @@ class BlocksAPITransformer(BlockStructureTransformer): BlockCountsTransformer BlockDepthTransformer BlockNavigationTransformer + ExtraFieldsTransformer Note: * BlockDepthTransformer must be executed before BlockNavigationTransformer. @@ -57,6 +59,7 @@ class BlocksAPITransformer(BlockStructureTransformer): BlockCountsTransformer.collect(block_structure) BlockDepthTransformer.collect(block_structure) BlockNavigationTransformer.collect(block_structure) + ExtraFieldsTransformer.collect(block_structure) # TODO support olx_data by calling export_to_xml(?) @@ -69,3 +72,4 @@ class BlocksAPITransformer(BlockStructureTransformer): BlockDepthTransformer(self.depth).transform(usage_info, block_structure) BlockNavigationTransformer(self.nav_depth).transform(usage_info, block_structure) VideoBlockURLTransformer().transform(usage_info, block_structure) + ExtraFieldsTransformer().transform(usage_info, block_structure) diff --git a/lms/djangoapps/course_api/blocks/transformers/extra_fields.py b/lms/djangoapps/course_api/blocks/transformers/extra_fields.py new file mode 100644 index 00000000000..636adeb0eb7 --- /dev/null +++ b/lms/djangoapps/course_api/blocks/transformers/extra_fields.py @@ -0,0 +1,69 @@ +""" +Extra Fields Transformer +""" +from django.conf import settings + +from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer + + +class ExtraFieldsTransformer(BlockStructureTransformer): + """ + A configurable transformer that adds additional XBlock fields to the course blocks API. + + Extra fields must be specified using the "COURSE_BLOCKS_API_EXTRA_FIELDS" + LMS Django settings variable. Open edX instances can use this to make + additional XBlock fields available via the course blocks API, that aren't + otherwise included by default. + """ + WRITE_VERSION = 1 + READ_VERSION = 1 + + @classmethod + def name(cls): + return "blocks_api:extra_fields" + + @classmethod + def collect(cls, block_structure): + """ + Collects any information that's necessary to execute this transformer's + transform method. + """ + requested_field_names = cls.get_requested_extra_fields() + + block_structure.request_xblock_fields('category', *requested_field_names) + + @classmethod + def get_requested_extra_fields(cls): + """ + Returns the names of the requested extra fields + """ + try: + return [field_name for block_type, field_name in settings.COURSE_BLOCKS_API_EXTRA_FIELDS] + except AttributeError: + return [] + + def transform(self, usage_info, block_structure): + """ + Mutates block_structure based on the given usage_info. + """ + if len(self.get_requested_extra_fields()) == 0: + return + + for block_key in block_structure.topological_traversal(): + for requested_block_type, requested_field_name in settings.COURSE_BLOCKS_API_EXTRA_FIELDS: + block_type = block_structure.get_xblock_field(block_key, 'category') + if (requested_block_type == '*' or + block_type in requested_block_type.split(',')): + requested_field_data = block_structure.get_xblock_field( + block_key, + requested_field_name, + None + ) + + if requested_field_data is not None: + block_structure.set_transformer_block_field( + block_key, + self, + requested_field_name, + requested_field_data + ) diff --git a/lms/djangoapps/course_api/blocks/transformers/tests/test_extra_fields.py b/lms/djangoapps/course_api/blocks/transformers/tests/test_extra_fields.py new file mode 100644 index 00000000000..e820cdd18dc --- /dev/null +++ b/lms/djangoapps/course_api/blocks/transformers/tests/test_extra_fields.py @@ -0,0 +1,61 @@ +""" +Tests for ExtraFieldsTransformer. +""" +from django.test import override_settings + +# pylint: disable=protected-access +from openedx.core.djangoapps.content.block_structure.factory import BlockStructureFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import SampleCourseFactory + +from ..extra_fields import ExtraFieldsTransformer + + +@override_settings(COURSE_BLOCKS_API_EXTRA_FIELDS=[('course', 'other_course_settings')]) +class TestExtraFieldsTransformer(ModuleStoreTestCase): + """ + Test proper behavior for ExtraFieldsTransformer + """ + shard = 4 + + OTHER_COURSE_SETTINGS_DEFAULT = { + 'test key': 'test value', + 'jackson 5': [ + ['a', 'b', 'c'], + 'it\'s easy as', + [1, 2, 3], + 'as simple as', + ['do', 're', 'mi'] + ] + } + + def setUp(self): + super(TestExtraFieldsTransformer, self).setUp() + + self.course = SampleCourseFactory.create( + other_course_settings=self.OTHER_COURSE_SETTINGS_DEFAULT + ) + self.course_key = self.course.id + + self.course_usage_key = self.store.make_course_usage_key(self.course_key) + self.block_structure = BlockStructureFactory.create_from_modulestore(self.course_usage_key, self.store) + + def test_transform(self): + # collect phase + ExtraFieldsTransformer.collect(self.block_structure) + self.block_structure._collect_requested_xblock_fields() + + # transform phase + ExtraFieldsTransformer().transform( + usage_info=None, + block_structure=self.block_structure, + ) + + block_data = self.block_structure.get_transformer_block_data( + self.course_usage_key, ExtraFieldsTransformer, + ) + + self.assertEqual( + block_data.other_course_settings, + self.OTHER_COURSE_SETTINGS_DEFAULT + ) diff --git a/lms/djangoapps/course_api/blocks/views.py b/lms/djangoapps/course_api/blocks/views.py index 54ae7683834..b9f1b9eb2e8 100644 --- a/lms/djangoapps/course_api/blocks/views.py +++ b/lms/djangoapps/course_api/blocks/views.py @@ -187,6 +187,10 @@ class BlocksView(DeveloperErrorViewMixin, ListAPIView): * show_correctness: Whether to show scores/correctness to learners for the current sequence or problem. Returned only if "show_correctness" is included in the "requested_fields" parameter. + + * Additional XBlock fields can be included in the response if they are + configured via the COURSE_BLOCKS_API_EXTRA_FIELDS Django setting and + requested via the "requested_fields" parameter. """ def list(self, request, usage_key_string, hide_access_denials=False): # pylint: disable=arguments-differ diff --git a/lms/envs/common.py b/lms/envs/common.py index 09cc70b8cd1..d29e173f412 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -488,6 +488,11 @@ FEATURES = { 'ENABLE_ORA_USERNAMES_ON_DATA_EXPORT': False, } +# Specifies extra XBlock fields that should available when requested via the Course Blocks API +# Should be a list of tuples of (block_type, field_name), where block_type can also be "*" for all block types. +# e.g. COURSE_BLOCKS_API_EXTRA_FIELDS = [ ('course', 'other_course_settings'), ("problem", "weight") ] +COURSE_BLOCKS_API_EXTRA_FIELDS = [] + # Settings for the course reviews tool template and identification key, set either to None to disable course reviews COURSE_REVIEWS_TOOL_PROVIDER_FRAGMENT_NAME = 'coursetalk-reviews-fragment.html' COURSE_REVIEWS_TOOL_PROVIDER_PLATFORM_KEY = 'edx' diff --git a/lms/envs/test.py b/lms/envs/test.py index 04eef4ee131..55553f765d4 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -486,6 +486,11 @@ FEATURES['ORGANIZATIONS_APP'] = True # Financial assistance page FEATURES['ENABLE_FINANCIAL_ASSISTANCE_FORM'] = True +COURSE_BLOCKS_API_EXTRA_FIELDS = [ + ('course', 'course_visibility'), + ('course', 'other_course_settings'), +] + COURSE_CATALOG_URL_ROOT = 'https://catalog.example.com' COURSE_CATALOG_API_URL = '{}/api/v1'.format(COURSE_CATALOG_URL_ROOT) -- GitLab