* feat: store content.child_usage_keys in Container search document Stores the draft children + published children (if applicable) Related fixes: * fix: lib_api.get_container does not take a "user" arg * refactor: fetch_customizable_fields_from_container does not need a "user" arg * refactor: moves tags_count into LibraryItem because anything that appears in a library may be tagged. * refactor: remove get_container_from_key from public API API users must use get_container and ContainerMetadata. * refactor: made set_library_item_collections take an entity_key string instead of a PublishableEntity instance, so we don't need to fetch a Container object to call it. * refactor: changed ContainerLink.update_or_create to take the container PK instead of a Container object, since this is enough. Added container_pk to ContainerMetadata to support this.
140 lines
6.1 KiB
Python
140 lines
6.1 KiB
Python
"""
|
|
Methods related to syncing a downstream XBlock with an upstream Container.
|
|
|
|
See upstream_sync.py for general upstream sync code that applies even when the
|
|
upstream is a container, not an XBlock.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import typing as t
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
from opaque_keys.edx.locator import LibraryContainerLocator
|
|
from xblock.core import XBlock
|
|
|
|
from openedx.core.djangoapps.content_libraries import api as lib_api
|
|
from .upstream_sync import UpstreamLink
|
|
|
|
if t.TYPE_CHECKING:
|
|
from django.contrib.auth.models import User # pylint: disable=imported-auth-user
|
|
|
|
|
|
def sync_from_upstream_container(
|
|
downstream: XBlock,
|
|
user: User,
|
|
) -> list[lib_api.LibraryXBlockMetadata | lib_api.ContainerMetadata]:
|
|
"""
|
|
Update `downstream` with content+settings from the latest available version of its linked upstream content.
|
|
|
|
Preserves overrides to customizable fields; overwrites overrides to other fields.
|
|
Does not save `downstream` to the store. That is left up to the caller.
|
|
|
|
If `downstream` lacks a valid+supported upstream link, this raises an UpstreamLinkException.
|
|
|
|
⭐️ Does not directly sync static assets (containers don't have them) nor
|
|
children. Returns a list of the upstream children so the caller can do that.
|
|
|
|
Should children be handled in here? Maybe if sync_from_upstream_block
|
|
were updated to handle static assets and also save changes to modulestore.
|
|
"""
|
|
link = UpstreamLink.get_for_block(downstream) # can raise UpstreamLinkException
|
|
if not isinstance(link.upstream_key, LibraryContainerLocator):
|
|
raise TypeError("sync_from_upstream_container() only supports Container upstreams, not containers")
|
|
lib_api.require_permission_for_library_key( # TODO: should permissions be checked at this low level?
|
|
link.upstream_key.lib_key,
|
|
user,
|
|
permission=lib_api.permissions.CAN_VIEW_THIS_CONTENT_LIBRARY,
|
|
)
|
|
upstream_meta = lib_api.get_container(link.upstream_key)
|
|
upstream_children = lib_api.get_container_children(link.upstream_key, published=True)
|
|
_update_customizable_fields(upstream=upstream_meta, downstream=downstream, only_fetch=False)
|
|
_update_non_customizable_fields(upstream=upstream_meta, downstream=downstream)
|
|
_update_tags(upstream=upstream_meta, downstream=downstream)
|
|
downstream.upstream_version = link.version_available
|
|
return upstream_children
|
|
|
|
|
|
def fetch_customizable_fields_from_container(*, downstream: XBlock) -> None:
|
|
"""
|
|
Fetch upstream-defined value of customizable fields and save them on the downstream.
|
|
|
|
The container version only retrieves values from *published* containers.
|
|
|
|
Basically, this sets the value of "upstream_display_name" on the downstream block.
|
|
"""
|
|
upstream = lib_api.get_container(LibraryContainerLocator.from_string(downstream.upstream))
|
|
_update_customizable_fields(upstream=upstream, downstream=downstream, only_fetch=True)
|
|
|
|
|
|
def _update_customizable_fields(*, upstream: lib_api.ContainerMetadata, downstream: XBlock, only_fetch: bool) -> None:
|
|
"""
|
|
For each customizable field:
|
|
* Save the upstream value to a hidden field on the downstream ("FETCH").
|
|
* If `not only_fetch`, and if the field *isn't* customized on the downstream, then:
|
|
* Update it the downstream field's value from the upstream field ("SYNC").
|
|
|
|
Concrete example: Imagine `lib_problem` is our upstream and `course_problem` is our downstream.
|
|
|
|
* Say that the customizable fields are [display_name, max_attempts].
|
|
|
|
* Set `course_problem.upstream_display_name = lib_problem.display_name` ("fetch").
|
|
* If `not only_fetch`, and `course_problem.display_name` wasn't customized, then:
|
|
* Set `course_problem.display_name = lib_problem.display_name` ("sync").
|
|
"""
|
|
# For now, the only supported container "field" is display_name
|
|
syncable_field_names = ["display_name"]
|
|
|
|
for field_name, fetch_field_name in downstream.get_customizable_fields().items():
|
|
|
|
if field_name not in syncable_field_names:
|
|
continue
|
|
|
|
# Downstream-only fields don't have an upstream fetch field
|
|
if fetch_field_name is None:
|
|
continue
|
|
|
|
# FETCH the upstream's value and save it on the downstream (ie, `downstream.upstream_$FIELD`).
|
|
old_upstream_value = getattr(downstream, fetch_field_name)
|
|
new_upstream_value = getattr(upstream, f"published_{field_name}")
|
|
setattr(downstream, fetch_field_name, new_upstream_value)
|
|
|
|
if only_fetch:
|
|
continue
|
|
|
|
# Okay, now for the nuanced part...
|
|
# We need to update the downstream field *iff it has not been customized**.
|
|
# Determining whether a field has been customized will differ in Beta vs Future release.
|
|
# (See "PRESERVING DOWNSTREAM CUSTOMIZATIONS" comment below for details.)
|
|
|
|
## FUTURE BEHAVIOR: field is "customized" iff we have noticed that the user edited it.
|
|
# if field_name in downstream.downstream_customized:
|
|
# continue
|
|
|
|
## BETA BEHAVIOR: field is "customized" iff we have the prev upstream value, but field doesn't match it.
|
|
downstream_value = getattr(downstream, field_name)
|
|
if old_upstream_value and downstream_value != old_upstream_value:
|
|
continue # Field has been customized. Don't touch it. Move on.
|
|
|
|
# Field isn't customized -- SYNC it!
|
|
setattr(downstream, field_name, new_upstream_value)
|
|
|
|
|
|
def _update_non_customizable_fields(*, upstream: lib_api.ContainerMetadata, downstream: XBlock) -> None:
|
|
"""
|
|
For each field `downstream.blah` that isn't customizable: set it to `upstream.blah`.
|
|
"""
|
|
# For now, there's nothing to do here - containers don't have any non-customizable fields.
|
|
|
|
|
|
def _update_tags(*, upstream: lib_api.ContainerMetadata, downstream: XBlock) -> None:
|
|
"""
|
|
Update tags from `upstream` to `downstream`
|
|
"""
|
|
from openedx.core.djangoapps.content_tagging.api import copy_tags_as_read_only
|
|
# For any block synced with an upstream, copy the tags as read_only
|
|
# This keeps tags added locally.
|
|
copy_tags_as_read_only(
|
|
str(upstream.container_key),
|
|
str(downstream.usage_key),
|
|
)
|