Skip to content

Commit

Permalink
Merge pull request #108 from open-craft/new-key-types
Browse files Browse the repository at this point in the history
Add New Blockstore-related Opaque Key Types
  • Loading branch information
David Ormsbee authored Sep 13, 2019
2 parents 72e54d1 + 2eae469 commit c33bd8f
Show file tree
Hide file tree
Showing 12 changed files with 778 additions and 18 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Unreleased


# 2.0

* Added LibraryLocatorV2 and LibraryUsageLocatorV2
* Added LearningContextKey, UsageKeyV2, and BundleDefinitionLocator
* Added the .is_course helper method

# 1.0.1

* Included test code in the PyPI package so its mixins can be imported into
other code

# 1.0.0

* Remove to_deprecated_string and from_deprecated_string API methods
Expand Down
9 changes: 7 additions & 2 deletions opaque_keys/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,14 @@ def from_string(cls, serialized):
cls._drivers()
try:
namespace, rest = cls._separate_namespace(serialized)
return cls.get_namespace_plugin(namespace)._from_string(rest)
key_class = cls.get_namespace_plugin(namespace)
if not issubclass(key_class, cls):
# CourseKey.from_string() should never return a non-course LearningContextKey,
# but they share the same namespace.
raise InvalidKeyError(cls, serialized)
return key_class._from_string(rest)
except InvalidKeyError:
if hasattr(cls, 'deprecated_fallback'):
if hasattr(cls, 'deprecated_fallback') and issubclass(cls.deprecated_fallback, cls):
return cls.deprecated_fallback._from_deprecated_string(serialized)
raise InvalidKeyError(cls, serialized)

Expand Down
15 changes: 14 additions & 1 deletion opaque_keys/edx/django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import six

from opaque_keys.edx.keys import BlockTypeKey, CourseKey, UsageKey
from opaque_keys.edx.keys import BlockTypeKey, CourseKey, LearningContextKey, UsageKey


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -178,6 +178,19 @@ def get_prep_lookup(self):
pass


class LearningContextKeyField(OpaqueKeyField):
"""
A django Field that stores a LearningContextKey object as a string.
If you know for certain that your code will only deal with courses, use
CourseKeyField instead, but if you are writing something more generic that
could apply to any learning context (libraries, etc.), use this instead of
CourseKeyField.
"""
description = "A LearningContextKey object, saved to the DB in the form of a string"
KEY_CLASS = LearningContextKey


class CourseKeyField(OpaqueKeyField):
"""
A django Field that stores a CourseKey object as a string.
Expand Down
94 changes: 92 additions & 2 deletions opaque_keys/edx/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,47 @@
"""
import json
from abc import abstractmethod, abstractproperty
import warnings
from six import text_type

from opaque_keys import OpaqueKey


class CourseKey(OpaqueKey):
class LearningContextKey(OpaqueKey):
"""
An :class:`opaque_keys.OpaqueKey` identifying a course, a library, a
program, a website, or some other collection of content where learning
happens.
This concept is more generic than "course."
A learning context does not necessarily have an org, course, or, run.
"""
KEY_TYPE = 'context_key'
__slots__ = ()

# is_course: subclasses should override this to indicate whether or not this
# key type represents a course (as opposed to a library or something else).
# We can't just use isinstance(key, CourseKey) because LibraryLocators
# are subclasses of CourseKey for historical reasons. Once modulestore-
# based content libraries are removed, one can replace this with
# just isinstance(key, CourseKey)
is_course = False

def make_definition_usage(self, definition_key, usage_id=None):
"""
Return a usage key, given the given the specified definition key and
usage_id.
"""
raise NotImplementedError()


class CourseKey(LearningContextKey):
"""
An :class:`opaque_keys.OpaqueKey` identifying a particular Course object.
"""
KEY_TYPE = 'course_key'
__slots__ = ()
is_course = True

@abstractproperty
def org(self): # pragma: no cover
Expand Down Expand Up @@ -157,6 +187,66 @@ def block_id(self):
"""
raise NotImplementedError()

@property
def context_key(self):
"""
Get the learning context key (LearningContextKey) for this XBlock usage.
"""
return self.course_key


class UsageKeyV2(UsageKey):
"""
An :class:`opaque_keys.OpaqueKey` identifying an XBlock used in a specific
learning context (e.g. a course).
Definition + Learning Context = Usage
UsageKeyV2 is just a subclass of UsageKey with slightly different behavior,
but not a distinct key type (same KEY_TYPE). UsageKeyV2 should be used for
new usage key types; the main differences between it and UsageKey are:
* the .course_key property is considered deprecated for the new V2 key
types, and they should implement .context_key instead.
* the .definition_key property is explicitly disabled for V2 usage keys
"""
__slots__ = ()

@abstractproperty
def context_key(self):
"""
Get the learning context key (LearningContextKey) for this XBlock usage.
May be a course key, library key, or some other LearningContextKey type.
"""
raise NotImplementedError()

@property
def definition_key(self):
"""
Returns the definition key for this usage. For the newer V2 key types,
this cannot be done with the key alone, so it's necessary to ask the
key's learning context to provide the underlying definition key.
"""
raise AttributeError(
"Version 2 usage keys do not support direct .definition_key access. "
"To get the definition key within edxapp, use: "
"get_learning_context_impl(usage_key).definition_for_usage(usage_key)"
)

@property
def course_key(self):
warnings.warn("Use .context_key instead of .course_key", DeprecationWarning, stacklevel=2)
return self.context_key

def map_into_course(self, course_key):
"""
Implement map_into_course for API compatibility. Shouldn't be used in
new code.
"""
if course_key == self.context_key:
return self
else:
raise ValueError("Cannot use map_into_course like that with this key type.")


class AsideDefinitionKey(DefinitionKey):
"""
Expand Down
Loading

0 comments on commit c33bd8f

Please sign in to comment.