-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5e1ca9c
commit 3bd5ef2
Showing
3 changed files
with
197 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
""" | ||
Synchronize content and settings from upstream blocks (in content libraries) to their | ||
downstream usages (in courses, etc.) | ||
At the time of writing, upstream blocks are assumed to come from content libraries. | ||
However, the XBlock fields are designed to be agnostic to their upstream's source context, | ||
so this assumption could be relaxed in the future if there is a need for upstreams from | ||
other kinds of learning contexts. | ||
""" | ||
from django.contrib.auth import get_user_model | ||
|
||
from opaque_keys.edx.keys import UsageKey | ||
from opaque_keys.edx.locator import LibraryUsageLocatorV2 | ||
from xblock.fields import String, Integer, List, Dict | ||
|
||
from openedx.core.djangoapps.content_libraries.api import get_library_block | ||
from openedx.core.djangoapps.xblock.api import load_block | ||
|
||
|
||
class UpstreamSyncMixin(XBlockMixin): | ||
""" | ||
@@TODO docstring | ||
""" | ||
|
||
upstream = 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_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_overridden = List( | ||
scope=Scope.settings, | ||
help=( | ||
"@@TODO helptext" | ||
), | ||
hidden=True, | ||
default=[], | ||
enforce_type=True, | ||
) | ||
upstream_settings = Dict( | ||
scope=Scope.settings, | ||
help=( | ||
"@@TODO helptext" | ||
), | ||
hidden=True, | ||
default={}, | ||
enforce_type=True, | ||
) | ||
|
||
def save(self, *args, **kwargs): | ||
""" | ||
@@TODO | ||
""" | ||
for field_name, value in self.upstream_settings.items(): | ||
if field_name not in self.upstream_overridden: | ||
if value != getattr(self, field_name): | ||
self.upstream_overridden.append(field_name) | ||
super().save() | ||
|
||
def set_upstream(self, upstream_key: LibraryUsageLocatorV2, user_id: int) -> None: | ||
""" | ||
Assign an upstream to this block and fetch upstream settings. | ||
Does not save block; caller must do so. | ||
""" | ||
self.upstream = str(upstream_key) | ||
try: | ||
self._sync_with_upstream(user_id=user_id, apply_updates=False) | ||
except exc: | ||
pass # @@TODO | ||
|
||
@XBlock.json_handler | ||
def upstream_info(self, _data=None, _suffix=None): | ||
""" | ||
@@TODO write this | ||
""" | ||
try: | ||
upstream_block, upstream_meta = self._load_upstream(user_id) | ||
except exc: | ||
pass # @@TODO | ||
return { | ||
"upstream": self.upstream, | ||
"available_version": upstream_meta.version_num, | ||
"available_version": ..., | ||
... update info ... | ||
} | ||
|
||
@XBlock.handler | ||
def update_from_upstream(self, request=None, suffix=None): | ||
""" | ||
@@TODO docstring | ||
""" | ||
try: | ||
user_id = requester.user.id if request and request.user else 0 | ||
self._sync_with_upstream(user_id=user_id, apply_updates=True) | ||
except NoUpsream: | ||
return Http204() # @@TODO what's the right response here? | ||
except BadUpstream as exc: | ||
return Http400BadRequest(exc.message) | ||
self.save() | ||
|
||
def _sync_with_upstream(self, *, user_id: int, apply_updates: bool) -> None: | ||
""" | ||
@@TODO docstring | ||
Does not save block; caller must do so. | ||
Can raise NoUpstream or BadUpstream. | ||
""" | ||
upstream, upstream_meta = self._load_upstream(user_id) | ||
self.upstream_settings = {} | ||
self.upstream_version = upstream_meta.version_num | ||
for field_name, field in upstream.fields.items(): | ||
if field.scope not in [Scope.settings, Scope.content]: | ||
continue | ||
value = getattr(upstream, field_name) | ||
if field.scope == Scope.settings: | ||
self.upstream_settings[field_name] = value | ||
if field_name in self.upstream_overridden: | ||
continue | ||
if not apply_updates: | ||
continue | ||
setattr(self, field_name, value) | ||
|
||
def _load_upstream(self, user_id: int) -> tuple[XBlock, LibraryXBlockMetadata]: | ||
""" | ||
@@TODO | ||
Can raise NoUpstream or BadUpstream. | ||
""" | ||
if not self.upstream: | ||
raise NoUpstream(self.usage_key) | ||
if not isinstance(self.upstream, str): | ||
raise BadUpstream(self.usage_key, self.upstream, "not a string") | ||
try: | ||
upstream_key = LibraryUsageLocatorV2.from_string(self.upstream) | ||
upstream = load_block(upstream_key, get_user_model().objects.get(id=user_id)) | ||
upstream_meta = get_library_block(upstream_key) | ||
return upstream, upstream_meta | ||
except InvalidKeyError: | ||
raise BadUpstream(self, "not a valid block usage key") | ||
except NotFound: | ||
raise BadUpstream(self, "block not found") | ||
except ContentLibraryBlockNotFound: | ||
raise BadUpstream(self, "block metadata not found") | ||
|
||
|
||
class NoUpstream(Exception): | ||
""" | ||
@@TODO | ||
""" | ||
def __init__(self, downstream_key: UpstreamSyncMixin): | ||
super().__init__(f"Block '{downstream_key}' is not associated with an upstream") | ||
|
||
|
||
class BadUpstream(Exception): | ||
""" | ||
@@TODO | ||
""" | ||
def __init__(self, downstream_key: Usagekey, upstream_key: object, message: str): | ||
super().__init__( | ||
f"Cannot fetch updates for block '{downstream_key}' " | ||
f"from upstream '{upstream_key}': {message}" | ||
) | ||
|
||
|
||
def is_block_valid_upstream(usage_key: UsageKey) -> bool: | ||
""" | ||
@@TODO move | ||
""" | ||
return isinstance(usage_key, LibraryUsageLocatorV2) |