Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 3.0.0

* Breaking: LibraryItemKey is replaced by CollectionKey and ContainerKey

# 2.13.0

* Breaking change to the new LibraryContainerLocator and
Expand Down
2 changes: 1 addition & 1 deletion opaque_keys/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from stevedore.enabled import EnabledExtensionManager

__version__ = '2.13.0'
__version__ = '3.0.0'


class InvalidKeyError(Exception):
Expand Down
26 changes: 24 additions & 2 deletions opaque_keys/edx/django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
If Django is unavailable, none of the classes below will work as intended.
"""
from __future__ import annotations

import logging
import warnings

Expand All @@ -17,8 +18,7 @@
IsNull = object

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

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

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -224,6 +224,28 @@ class UsageKeyField(OpaqueKeyField):
_pyi_private_get_type: UsageKey | None


class ContainerKeyField(OpaqueKeyField):
"""
A django Field that stores a ContainerKey object as a string.
"""
description = "A Location object, saved to the DB in the form of a string"
KEY_CLASS = ContainerKey
# Declare the field types for the django-stubs mypy type hint plugin:
_pyi_private_set_type: ContainerKey | str | None
_pyi_private_get_type: ContainerKey | None


class CollectionKeyField(OpaqueKeyField):
"""
A django Field that stores a CollectionKey object as a string.
"""
description = "A Location object, saved to the DB in the form of a string"
KEY_CLASS = CollectionKey
# Declare the field types for the django-stubs mypy type hint plugin:
_pyi_private_set_type: CollectionKey | str | None
_pyi_private_get_type: CollectionKey | None


class LocationKeyField(UsageKeyField):
"""
A django Field that stores a UsageKey object as a string.
Expand Down
9 changes: 8 additions & 1 deletion opaque_keys/edx/django/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
Model = object

from opaque_keys.edx.django.models import (
BlockTypeKeyField, CourseKeyField, CreatorMixin, UsageKeyField
BlockTypeKeyField,
CollectionKeyField,
ContainerKeyField,
CourseKeyField,
CreatorMixin,
UsageKeyField,
)


Expand Down Expand Up @@ -64,3 +69,5 @@ class ComplexModel(Model):
course_key = CourseKeyField(max_length=255, validators=[is_edx])
block_type_key = BlockTypeKeyField(max_length=255, blank=True)
usage_key = UsageKeyField(max_length=255, blank=False)
collection_key = CollectionKeyField(max_length=255, blank=False)
container_key = ContainerKeyField(max_length=255, blank=False)
8 changes: 6 additions & 2 deletions opaque_keys/edx/django/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import pytest

from opaque_keys.edx.django.models import OpaqueKeyField, UsageKeyField
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.keys import CollectionKey, ContainerKey, CourseKey, UsageKey

from .models import ComplexModel, Container, ExampleModel

Expand Down Expand Up @@ -68,10 +68,14 @@ def setUp(self):
super().setUp()
self.course_key = CourseKey.from_string('course-v1:edX+FUN101x+3T2017')
self.usage_key = UsageKey.from_string('block-v1:edX+FUN101x+3T2017+type@html+block@12345678')
self.collection_key = CollectionKey.from_string('lib-collection:TestX:LibraryX:test-problem-bank')
self.container_key = ContainerKey.from_string('lct:TestX:LibraryX:unit:test-container')
self.model = ComplexModel(
id='foobar',
course_key=self.course_key,
usage_key=self.usage_key
usage_key=self.usage_key,
collection_key=self.collection_key,
container_key=self.container_key,
)
self.model.save()

Expand Down
63 changes: 35 additions & 28 deletions opaque_keys/edx/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@
from __future__ import annotations
import json
from abc import abstractmethod
from typing import TYPE_CHECKING, Self
from typing import Self
import warnings

from typing_extensions import deprecated # For python 3.13+ can use 'from warnings import deprecated'

from opaque_keys import OpaqueKey

if TYPE_CHECKING:
from opaque_keys.edx.locator import LibraryLocatorV2
Comment on lines -14 to -15
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refactor allows us to get rid of this messy import of a locator (subclass) in the keys (parent class) file.



class LearningContextKey(OpaqueKey): # pylint: disable=abstract-method
"""
Expand Down Expand Up @@ -93,28 +88,6 @@ def make_asset_key(self, asset_type: str, path: str) -> AssetKey: # pragma: no
raise NotImplementedError()


class LibraryItemKey(OpaqueKey):
"""
An :class:`opaque_keys.OpaqueKey` identifying a particular item in a library.
"""
KEY_TYPE = 'library_item_key'
lib_key: LibraryLocatorV2
__slots__ = ()

@property
@abstractmethod
def org(self) -> str | None: # pragma: no cover
"""
The organization that this object belongs to.
"""
raise NotImplementedError()

@property
@deprecated("Use lib_key instead")
def library_key(self):
return self.lib_key


class DefinitionKey(OpaqueKey):
"""
An :class:`opaque_keys.OpaqueKey` identifying an XBlock definition.
Expand Down Expand Up @@ -281,6 +254,40 @@ def map_into_course(self, course_key: CourseKey) -> Self:
raise ValueError("Cannot use map_into_course like that with this key type.")


class ContainerKey(OpaqueKey):
"""
An :class:`opaque_keys.OpaqueKey` identifying a container (a non-XBlock
structure like a unit, section, or subsection).
"""
KEY_TYPE = 'container_key'
__slots__ = ()

@property
@abstractmethod
def context_key(self) -> LearningContextKey: # pragma: no cover
"""
Get the learning context key (LearningContextKey) for this container.
"""
raise NotImplementedError()


class CollectionKey(OpaqueKey):
"""
An :class:`opaque_keys.OpaqueKey` identifying a collection (a group of
content, mostly used in libraries to organize components/containers).
"""
KEY_TYPE = 'collection_key'
__slots__ = ()

@property
@abstractmethod
def context_key(self) -> LearningContextKey: # pragma: no cover
"""
Get the learning context key (LearningContextKey) for this collection.
"""
raise NotImplementedError()


class AsideDefinitionKey(DefinitionKey):
"""
A definition key for an aside.
Expand Down
16 changes: 13 additions & 3 deletions opaque_keys/edx/locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from opaque_keys import OpaqueKey, InvalidKeyError
from opaque_keys.edx.keys import AssetKey, CourseKey, DefinitionKey, \
LearningContextKey, UsageKey, UsageKeyV2, LibraryItemKey
LearningContextKey, UsageKey, UsageKeyV2, ContainerKey, CollectionKey

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -1622,13 +1622,14 @@ def html_id(self) -> str:
return str(self)


class LibraryCollectionLocator(CheckFieldMixin, LibraryItemKey):
class LibraryCollectionLocator(CheckFieldMixin, CollectionKey):
"""
When serialized, these keys look like:
lib-collection:org:lib:collection-id
"""
CANONICAL_NAMESPACE = 'lib-collection'
KEY_FIELDS = ('lib_key', 'collection_id')
lib_key: LibraryLocatorV2
collection_id: str

__slots__ = KEY_FIELDS
Expand Down Expand Up @@ -1675,14 +1676,19 @@ def _from_string(cls, serialized: str) -> Self:
except (ValueError, TypeError) as error:
raise InvalidKeyError(cls, serialized) from error

@property
def context_key(self) -> LibraryLocatorV2:
return self.lib_key


class LibraryContainerLocator(CheckFieldMixin, LibraryItemKey):
class LibraryContainerLocator(CheckFieldMixin, ContainerKey):
"""
When serialized, these keys look like:
lct:org:lib:ct-type:ct-id
"""
CANONICAL_NAMESPACE = 'lct' # "Library Container"
KEY_FIELDS = ('lib_key', 'container_type', 'container_id')
lib_key: LibraryLocatorV2
container_type: str
container_id: str

Expand Down Expand Up @@ -1714,6 +1720,10 @@ def org(self) -> str | None: # pragma: no cover
"""
return self.lib_key.org

@property
def context_key(self) -> LibraryLocatorV2:
return self.lib_key

def _to_string(self) -> str:
"""
Serialize this key as a string
Expand Down
25 changes: 15 additions & 10 deletions opaque_keys/edx/tests/test_collection_locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ddt
from opaque_keys import InvalidKeyError
from opaque_keys.edx.tests import LocatorBaseTest
from opaque_keys.edx.keys import CollectionKey
from opaque_keys.edx.locator import LibraryCollectionLocator, LibraryLocatorV2


Expand Down Expand Up @@ -32,11 +33,12 @@ def test_coll_key_constructor(self):
lib_key = LibraryLocatorV2(org=org, slug=lib)
coll_key = LibraryCollectionLocator(lib_key=lib_key, collection_id=code)
lib_key = coll_key.lib_key
self.assertEqual(str(coll_key), "lib-collection:TestX:LibraryX:test-problem-bank")
self.assertEqual(coll_key.org, org)
self.assertEqual(coll_key.collection_id, code)
self.assertEqual(lib_key.org, org)
self.assertEqual(lib_key.slug, lib)
assert str(coll_key) == "lib-collection:TestX:LibraryX:test-problem-bank"
assert coll_key.org == org
assert coll_key.collection_id == code
assert lib_key.org == org
assert lib_key.slug == lib
assert isinstance(coll_key, CollectionKey)

def test_coll_key_constructor_bad_ids(self):
lib_key = LibraryLocatorV2(org="TestX", slug="lib1")
Expand All @@ -52,12 +54,15 @@ def test_coll_key_from_string(self):
code = 'test-problem-bank'
str_key = f"lib-collection:{org}:{lib}:{code}"
coll_key = LibraryCollectionLocator.from_string(str_key)
assert coll_key == CollectionKey.from_string(str_key)
assert str(coll_key) == str_key
assert coll_key.org == org
assert coll_key.collection_id == code
lib_key = coll_key.lib_key
self.assertEqual(str(coll_key), str_key)
self.assertEqual(coll_key.org, org)
self.assertEqual(coll_key.collection_id, code)
self.assertEqual(lib_key.org, org)
self.assertEqual(lib_key.slug, lib)
assert isinstance(lib_key, LibraryLocatorV2)
assert lib_key.org == org
assert lib_key.slug == lib
assert coll_key.context_key == lib_key

def test_coll_key_invalid_from_string(self):
with self.assertRaises(InvalidKeyError):
Expand Down
29 changes: 17 additions & 12 deletions opaque_keys/edx/tests/test_container_locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import ddt
from opaque_keys import InvalidKeyError
from opaque_keys.edx.tests import LocatorBaseTest
from opaque_keys.edx.keys import ContainerKey
from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2


Expand Down Expand Up @@ -37,12 +38,13 @@ def test_key_constructor(self):
container_id=container_id,
)
lib_key = container_key.lib_key
self.assertEqual(str(container_key), "lct:TestX:LibraryX:unit:test-container")
self.assertEqual(container_key.org, org)
self.assertEqual(container_key.container_type, container_type)
self.assertEqual(container_key.container_id, container_id)
self.assertEqual(lib_key.org, org)
self.assertEqual(lib_key.slug, lib)
assert str(container_key) == "lct:TestX:LibraryX:unit:test-container"
assert container_key.org == org
assert container_key.container_type == container_type
assert container_key.container_id == container_id
assert lib_key.org == org
assert lib_key.slug == lib
assert isinstance(container_key, ContainerKey)

def test_key_constructor_bad_ids(self):
lib_key = LibraryLocatorV2(org="TestX", slug="lib1")
Expand All @@ -66,10 +68,13 @@ def test_key_from_string(self):
container_id = 'test-container'
str_key = f"lct:{org}:{lib}:{container_type}:{container_id}"
container_key = LibraryContainerLocator.from_string(str_key)
assert container_key == ContainerKey.from_string(str_key)
assert str(container_key) == str_key
assert container_key.org == org
assert container_key.container_type == container_type
assert container_key.container_id == container_id
lib_key = container_key.lib_key
self.assertEqual(str(container_key), str_key)
self.assertEqual(container_key.org, org)
self.assertEqual(container_key.container_type, container_type)
self.assertEqual(container_key.container_id, container_id)
self.assertEqual(lib_key.org, org)
self.assertEqual(lib_key.slug, lib)
assert isinstance(lib_key, LibraryLocatorV2)
assert lib_key.org == org
assert lib_key.slug == lib
assert container_key.context_key == lib_key
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,10 @@ def get_version(*file_paths):
'block_type': [
'block-type-v1 = opaque_keys.edx.block_types:BlockTypeKeyV1',
],
'library_item_key': [
'collection_key': [
'lib-collection = opaque_keys.edx.locator:LibraryCollectionLocator',
],
'container_key': [
'lct = opaque_keys.edx.locator:LibraryContainerLocator',
],
}
Expand Down