After we merged this PR: https://github.com/openedx/edx-platform/pull/33920 this error began popping up in logs: Unable to load XBlock 'staffgradedxblock' .... ImportError: cannot import name 'get_course_blocks' from partially initialized module 'lms.djangoapps.course_blocks.api' (most likely due to a circular import) ... The root cause was the new imports of `derived_key` and `BlockKey` into xmodule/library_content_block.py. Those new imports come from xmodule/modulestore/store_utilities.py, which runs `XBlock.load_classes()` at the module level, which fails because we are still in the process of loading xmodule/library_content_block. As a solution, we move both `derived_key` and `BlockKey` to xmodule/util/keys.py. We could potentially move that file to opaque-keys eventually, depending on how well we think that those concepts generalize. Also: * We rename the function from derived_key to derive_key, as functions should be verbs. * We combine the first to parameters of derive_key (a source ContextKey and a source BlockKey) into a single parameter (a source UsageKey). In my opinion, this makes the function call easier to understand.
63 lines
2.6 KiB
Python
63 lines
2.6 KiB
Python
"""
|
|
Utilities for working with opaque-keys.
|
|
|
|
Consider moving these into opaque-keys if they generalize well.
|
|
"""
|
|
import hashlib
|
|
from typing import NamedTuple
|
|
|
|
|
|
from opaque_keys.edx.keys import UsageKey
|
|
|
|
|
|
class BlockKey(NamedTuple):
|
|
"""
|
|
A pair of strings (type, id) that uniquely identify an XBlock Usage within a LearningContext.
|
|
|
|
Put another way: LearningContextKey * BlockKey = UsageKey.
|
|
|
|
Example:
|
|
"course-v1:myOrg+myCourse+myRun" <- LearningContextKey string
|
|
("html", "myBlock") <- BlockKey
|
|
"course-v1:myOrg+myCourse+myRun+type@html+block@myBlock" <- UsageKey string
|
|
"""
|
|
type: str
|
|
id: str
|
|
|
|
@classmethod
|
|
def from_usage_key(cls, usage_key):
|
|
return cls(usage_key.block_type, usage_key.block_id)
|
|
|
|
|
|
def derive_key(source: UsageKey, dest_parent: BlockKey) -> BlockKey:
|
|
"""
|
|
Return a new reproducible BlockKey for a given source usage and destination parent block.
|
|
|
|
When recursively copying a block structure, we need to generate new block IDs for the
|
|
blocks. We don't want to use the exact same IDs as we might copy blocks multiple times.
|
|
However, we do want to be able to reproduce the same IDs when copying the same block
|
|
so that if we ever need to re-copy the block from its source (that is, to update it with
|
|
upstream changes) we don't affect any data tied to the ID, such as grades.
|
|
|
|
This is used by the copy_from_template function of the modulestore, and can be used by
|
|
pluggable django apps that need to copy blocks from one course to another in an
|
|
idempotent way. In the future, this should be created into a proper API function
|
|
in the spirit of OEP-49.
|
|
"""
|
|
source_context = source.context_key
|
|
if hasattr(source_context, 'for_version'):
|
|
source_context = source_context.for_version(None)
|
|
# Compute a new block ID. This new block ID must be consistent when this
|
|
# method is called with the same (source, dest_parent) pair.
|
|
# Note: years after this was written, mypy pointed out that the way we are
|
|
# encoding & formatting the source context means it looks like b'....', ie
|
|
# it literally contains the character 'b' and single quotes within the unique_data
|
|
# string. So that's a little silly, but it's fine, and we can't change it now.
|
|
unique_data = "{}:{}:{}".format(
|
|
str(source_context).encode("utf-8"), # type: ignore[str-bytes-safe]
|
|
source.block_id,
|
|
dest_parent.id,
|
|
)
|
|
new_block_id = hashlib.sha1(unique_data.encode('utf-8')).hexdigest()[:20]
|
|
return BlockKey(source.block_type, new_block_id)
|