From f9e6dcb1035d81969661e33cbd9995f95337c933 Mon Sep 17 00:00:00 2001 From: "Kyle D. McCormick" Date: Thu, 23 May 2024 17:03:50 -0400 Subject: [PATCH] feat: upstream_block fields prototype (WIP) --- cms/djangoapps/contentstore/helpers.py | 20 +++++++++-- cms/lib/xblock/authoring_mixin.py | 36 ++++++++++++++++++- .../core/djangoapps/content_libraries/api.py | 5 ++- xmodule/modulestore/inheritance.py | 12 ++++++- .../split_mongo/split_mongo_kvs.py | 2 +- 5 files changed, 68 insertions(+), 7 deletions(-) diff --git a/cms/djangoapps/contentstore/helpers.py b/cms/djangoapps/contentstore/helpers.py index a4ece6c85d59..2e32594322a8 100644 --- a/cms/djangoapps/contentstore/helpers.py +++ b/cms/djangoapps/contentstore/helpers.py @@ -11,9 +11,9 @@ from django.conf import settings from django.utils.translation import gettext as _ from opaque_keys.edx.keys import AssetKey, CourseKey, UsageKey -from opaque_keys.edx.locator import DefinitionLocator, LocalId +from opaque_keys.edx.locator import DefinitionLocator, LocalId, LibraryUsageLocatorV2 from xblock.core import XBlock -from xblock.fields import ScopeIds +from xblock.fields import Scope, ScopeIds from xblock.runtime import IdGenerator from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore @@ -23,6 +23,7 @@ from cms.djangoapps.models.settings.course_grading import CourseGradingModel from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers +from openedx.core.djangoapps.content_libraries.api import get_library_block import openedx.core.djangoapps.content_staging.api as content_staging_api import openedx.core.djangoapps.content_tagging.api as content_tagging_api @@ -293,7 +294,6 @@ def import_staged_content_from_user_clipboard(parent_key: UsageKey, request) -> staged_content_id=user_clipboard.content.id, static_files=static_files, ) - return new_xblock, notices @@ -375,6 +375,20 @@ def _import_xml_node_to_parent( if copied_from_block: # Store a reference to where this block was copied from, in the 'copied_from_block' field (AuthoringMixin) temp_xblock.copied_from_block = copied_from_block + # If it was copied from a library, set the 'upstream_*' fields (AuthoringMixin) + copied_from_key = UsageKey.from_string(copied_from_block) # @@TODO: param should be UsageKey, not str + if isinstance(copied_from_key, LibraryUsageLocatorV2): + temp_xblock.upstream_block = copied_from_block + temp_xblock.upstream_block_version = get_library_block(copied_from_key).version_num # @@TODO: handle miss? + from openedx.core.djangoapps.xblock.api import load_block + from django.contrib.auth import get_user_model + upstream_xblock = load_block(copied_from_key, get_user_model().objects.get(id=user_id)) + print(temp_xblock.upstream_block_settings) + temp_xblock.upstream_block_settings = { + field.name: getattr(upstream_xblock, field.name) + for field in upstream_xblock.fields + if field.scope == Scope.settings + } # Save the XBlock into modulestore. We need to save the block and its parent for this to work: new_xblock = store.update_item(temp_xblock, user_id, allow_not_found=True) parent_xblock.children.append(new_xblock.location) diff --git a/cms/lib/xblock/authoring_mixin.py b/cms/lib/xblock/authoring_mixin.py index a3d3b3298cea..bb2df06582bb 100644 --- a/cms/lib/xblock/authoring_mixin.py +++ b/cms/lib/xblock/authoring_mixin.py @@ -8,7 +8,7 @@ from django.conf import settings from web_fragments.fragment import Fragment from xblock.core import XBlock, XBlockMixin -from xblock.fields import String, Scope +from xblock.fields import Integer, String, Scope, Dict log = logging.getLogger(__name__) @@ -51,3 +51,37 @@ def visibility_view(self, _context=None): scope=Scope.settings, enforce_type=True, ) + + # Note: upstream_* fields are only used by CMS. Not needed in the LMS. + upstream_block = String( + scope=Scope.settings, + help=( + "The usage key of a block (generally within a Content Library) which serves as a source of upstream " + "updates for this block, or None if there is no such upstream. Please note: It is valid for upstream_block " + "to hold a usage key for a block that does not exist (or does not *yet* exist) on this instance, " + "particularly if this block was imported from a different instance." + ), + hidden=True, + default=None, + enforce_type=True, + ) + upstream_block_version = Integer( + scope=Scope.settings, + help=( + "The upstream_block's version number, at the time this block was created from it. " + "If this version is older than the upstream_block's latest version, then CMS will " + "allow this block to fetch updated content from upstream_block." + ), + hidden=True, + default=None, + enforce_type=True, + ) + upstream_block_settings = Dict( + scope=Scope.settings, + help=( + "@@TODO" + ), + hidden=True, + default={}, + enforce_type=True, + ) diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index 82d648eaeba1..c84f2e91dfde 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -194,6 +194,7 @@ class LibraryXBlockMetadata: Class that represents the metadata about an XBlock in a content library. """ usage_key = attr.ib(type=LibraryUsageLocatorV2) + version_num = attr.ib(type=int) display_name = attr.ib("") has_unpublished_changes = attr.ib(False) tags_count = attr.ib(0) @@ -210,7 +211,8 @@ def from_component(cls, library_key, component): component.local_key, ), display_name=component.versioning.draft.title, - has_unpublished_changes=component.versioning.has_unpublished_changes + has_unpublished_changes=component.versioning.has_unpublished_changes, + version_num=component.versioning.draft.version_num, ) @@ -651,6 +653,7 @@ def get_library_block(usage_key) -> LibraryXBlockMetadata: usage_key=usage_key, display_name=draft_version.title, has_unpublished_changes=(draft_version != published_version), + version_num=draft_version.version_num, ) diff --git a/xmodule/modulestore/inheritance.py b/xmodule/modulestore/inheritance.py index 4c5a14b769cb..6040ec9a0131 100644 --- a/xmodule/modulestore/inheritance.py +++ b/xmodule/modulestore/inheritance.py @@ -397,6 +397,11 @@ def default(self, block, name): return field.read_json(ancestor) else: ancestor = ancestor.get_parent() + # @@TODO de-kludgify, move to core or mixin? + try: + return block.fields["upstream_block_settings"][name] + except KeyError: + pass return super().default(block, name) @@ -448,4 +453,9 @@ def default(self, key): inheriting, this will raise KeyError which will cause the caller to use the field's global default. """ - return self.inherited_settings[key.field_name] + try: + return self.inherited_settings[key.field_name] + except KeyError: + pass + # @@TODO de-kludgify, move to its own mixin, or core? + return self._fields.get("upstream_block_settings", {})[key.field_name] diff --git a/xmodule/modulestore/split_mongo/split_mongo_kvs.py b/xmodule/modulestore/split_mongo/split_mongo_kvs.py index 0c321232079f..381bc568d8dc 100644 --- a/xmodule/modulestore/split_mongo/split_mongo_kvs.py +++ b/xmodule/modulestore/split_mongo/split_mongo_kvs.py @@ -163,7 +163,7 @@ def has_default_value(self, field_name): """ Is the given field has default value in this kvs """ - return field_name in self._defaults + return field_name in self._defaults or super().has_default_value(field_name) def default(self, key): """