From fcff2e98448a77f29506fda7192733fcd562c85a Mon Sep 17 00:00:00 2001 From: Kyle McCormick Date: Wed, 24 Jan 2024 09:33:56 -0500 Subject: [PATCH] feat: XBlock.usage_key, XBlock.context_key convenience props (#690) Add two new properties to `XBlock` objects: `.usage_key` and `.context_key`. These simply expose the values of `.scope_ids.usage_id`` and `.scope_ids.usage_id.context_key`, providing a convenient replacement to the deprecated, edx-platform-specific block properties `.location` and `.course_id`, respectively. Note: this adds opaque-keys as a dependency for some new unit tests. Normally it wouldn't make sense to add a dependency just for a couple of tests, but we anticipate to make the repo depend on opaque-keys: soon anyway, for: * https://github.com/openedx/XBlock/issues/707, and * https://github.com/openedx/XBlock/issues/708 Bumps version from 1.9.1 to 1.10.0 --- CHANGELOG.rst | 8 ++++++++ requirements/base.in | 1 + requirements/base.txt | 12 +++++++++++- requirements/dev.txt | 16 ++++++++++++---- requirements/django.txt | 24 ++++++++++++++++++++--- requirements/doc.txt | 25 ++++++++++++++++++++---- requirements/test.txt | 25 ++++++++++++++++++------ xblock/__init__.py | 2 +- xblock/core.py | 26 +++++++++++++++++++++++++ xblock/test/test_core.py | 41 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 161 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c81085e9..05f21a2c4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,14 @@ These are notable changes in XBlock. Unreleased ---------- +1.10.0 - 2024-01-12 +------------------- + +* Add two new properties to ``XBlock`` objects: ``.usage_key`` and ``.context_key``. + These simply expose the values of ``.scope_ids.usage_id`` and ``.scope_ids.usage_id.context_key``, + providing a convenient replacement to the deprecated, edx-platform-specific block properties ``.location`` + and ``.course_id``, respectively. + 1.9.1 - 2023-12-22 ------------------ diff --git a/requirements/base.in b/requirements/base.in index 5bb2941e0..07b5047d2 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -1,6 +1,7 @@ # Core requirements for using this package -c constraints.txt +edx-opaque-keys fs lxml mako diff --git a/requirements/base.txt b/requirements/base.txt index 4bbc458db..a4fa671c4 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,16 +6,22 @@ # appdirs==1.4.4 # via fs +edx-opaque-keys==2.5.1 + # via -r requirements/base.in fs==2.4.16 # via -r requirements/base.in lxml==5.1.0 # via -r requirements/base.in -mako==1.3.0 +mako==1.3.1 # via -r requirements/base.in markupsafe==2.1.4 # via # -r requirements/base.in # mako +pbr==6.0.0 + # via stevedore +pymongo==3.13.0 + # via edx-opaque-keys python-dateutil==2.8.2 # via -r requirements/base.in pytz==2023.3.post1 @@ -28,6 +34,10 @@ six==1.16.0 # via # fs # python-dateutil +stevedore==5.1.0 + # via edx-opaque-keys +typing-extensions==4.9.0 + # via edx-opaque-keys web-fragments==2.1.0 # via -r requirements/base.in webob==1.8.7 diff --git a/requirements/dev.txt b/requirements/dev.txt index 81c2a58e5..6a38c4d52 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -21,11 +21,11 @@ attrs==23.2.0 # via # -r requirements/test.txt # hypothesis -boto3==1.34.23 +boto3==1.34.26 # via # -r requirements/test.txt # fs-s3fs -botocore==1.34.23 +botocore==1.34.26 # via # -r requirements/test.txt # boto3 @@ -93,6 +93,8 @@ django==2.2.28 # openedx-django-pyfs edx-lint==5.3.6 # via -r requirements/test.txt +edx-opaque-keys==2.5.1 + # via -r requirements/test.txt exceptiongroup==1.2.0 # via # -r requirements/test.txt @@ -113,7 +115,7 @@ fs-s3fs==1.1.1 # via # -r requirements/test.txt # openedx-django-pyfs -hypothesis==6.96.1 +hypothesis==6.96.4 # via -r requirements/test.txt importlib-metadata==7.0.1 # via @@ -150,7 +152,7 @@ lazy==1.6 # via -r requirements/test.txt lxml==5.1.0 # via -r requirements/test.txt -mako==1.3.0 +mako==1.3.1 # via -r requirements/test.txt markupsafe==2.1.4 # via @@ -230,6 +232,10 @@ pylint-plugin-utils==0.8.2 # -r requirements/test.txt # pylint-celery # pylint-django +pymongo==3.13.0 + # via + # -r requirements/test.txt + # edx-opaque-keys pyproject-api==1.6.1 # via # -r requirements/ci.txt @@ -289,6 +295,7 @@ stevedore==5.1.0 # via # -r requirements/test.txt # code-annotations + # edx-opaque-keys text-unidecode==1.3 # via # -r requirements/test.txt @@ -319,6 +326,7 @@ typing-extensions==4.9.0 # -r requirements/test.txt # annotated-types # astroid + # edx-opaque-keys # inflect # pydantic # pydantic-core diff --git a/requirements/django.txt b/requirements/django.txt index 82c948821..b0da24087 100644 --- a/requirements/django.txt +++ b/requirements/django.txt @@ -8,9 +8,9 @@ appdirs==1.4.4 # via # -r requirements/base.txt # fs -boto3==1.34.23 +boto3==1.34.26 # via fs-s3fs -botocore==1.34.23 +botocore==1.34.26 # via # boto3 # s3transfer @@ -19,6 +19,8 @@ django==2.2.28 # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/django.in # openedx-django-pyfs +edx-opaque-keys==2.5.1 + # via -r requirements/base.txt fs==2.4.16 # via # -r requirements/base.txt @@ -34,7 +36,7 @@ lazy==1.6 # via -r requirements/django.in lxml==5.1.0 # via -r requirements/base.txt -mako==1.3.0 +mako==1.3.1 # via -r requirements/base.txt markupsafe==2.1.4 # via @@ -42,6 +44,14 @@ markupsafe==2.1.4 # mako openedx-django-pyfs==3.4.1 # via -r requirements/django.in +pbr==6.0.0 + # via + # -r requirements/base.txt + # stevedore +pymongo==3.13.0 + # via + # -r requirements/base.txt + # edx-opaque-keys python-dateutil==2.8.2 # via # -r requirements/base.txt @@ -64,6 +74,14 @@ six==1.16.0 # python-dateutil sqlparse==0.4.4 # via django +stevedore==5.1.0 + # via + # -r requirements/base.txt + # edx-opaque-keys +typing-extensions==4.9.0 + # via + # -r requirements/base.txt + # edx-opaque-keys urllib3==1.26.18 # via botocore web-fragments==2.1.0 diff --git a/requirements/doc.txt b/requirements/doc.txt index 4d83f7253..e864c8dfb 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -18,11 +18,11 @@ babel==2.14.0 # sphinx beautifulsoup4==4.12.3 # via pydata-sphinx-theme -boto3==1.34.23 +boto3==1.34.26 # via # -r requirements/django.txt # fs-s3fs -botocore==1.34.23 +botocore==1.34.26 # via # -r requirements/django.txt # boto3 @@ -40,6 +40,8 @@ docutils==0.19 # via # pydata-sphinx-theme # sphinx +edx-opaque-keys==2.5.1 + # via -r requirements/django.txt fs==2.4.16 # via # -r requirements/django.txt @@ -66,7 +68,7 @@ lazy==1.6 # via -r requirements/django.txt lxml==5.1.0 # via -r requirements/django.txt -mako==1.3.0 +mako==1.3.1 # via -r requirements/django.txt markupsafe==2.1.4 # via @@ -81,6 +83,10 @@ packaging==23.2 # via # pydata-sphinx-theme # sphinx +pbr==6.0.0 + # via + # -r requirements/django.txt + # stevedore pydata-sphinx-theme==0.14.4 # via sphinx-book-theme pygments==2.17.2 @@ -88,6 +94,10 @@ pygments==2.17.2 # accessible-pygments # pydata-sphinx-theme # sphinx +pymongo==3.13.0 + # via + # -r requirements/django.txt + # edx-opaque-keys python-dateutil==2.8.2 # via # -r requirements/django.txt @@ -140,8 +150,15 @@ sqlparse==0.4.4 # via # -r requirements/django.txt # django +stevedore==5.1.0 + # via + # -r requirements/django.txt + # edx-opaque-keys typing-extensions==4.9.0 - # via pydata-sphinx-theme + # via + # -r requirements/django.txt + # edx-opaque-keys + # pydata-sphinx-theme urllib3==1.26.18 # via # -r requirements/django.txt diff --git a/requirements/test.txt b/requirements/test.txt index 1afd656ae..64034b310 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -17,11 +17,11 @@ astroid==3.0.2 # pylint-celery attrs==23.2.0 # via hypothesis -boto3==1.34.23 +boto3==1.34.26 # via # -r requirements/django.txt # fs-s3fs -botocore==1.34.23 +botocore==1.34.26 # via # -r requirements/django.txt # boto3 @@ -61,6 +61,8 @@ distlib==0.3.8 # openedx-django-pyfs edx-lint==5.3.6 # via -r requirements/test.in +edx-opaque-keys==2.5.1 + # via -r requirements/django.txt exceptiongroup==1.2.0 # via # hypothesis @@ -78,7 +80,7 @@ fs-s3fs==1.1.1 # via # -r requirements/django.txt # openedx-django-pyfs -hypothesis==6.96.1 +hypothesis==6.96.4 # via -r requirements/test.in inflect==7.0.0 # via jinja2-pluralize @@ -102,7 +104,7 @@ lazy==1.6 # via -r requirements/django.txt lxml==5.1.0 # via -r requirements/django.txt -mako==1.3.0 +mako==1.3.1 # via -r requirements/django.txt markupsafe==2.1.4 # via @@ -123,7 +125,9 @@ packaging==23.2 path==16.9.0 # via -r requirements/test.in pbr==6.0.0 - # via stevedore + # via + # -r requirements/django.txt + # stevedore platformdirs==4.1.0 # via # pylint @@ -157,6 +161,10 @@ pylint-plugin-utils==0.8.2 # via # pylint-celery # pylint-django +pymongo==3.13.0 + # via + # -r requirements/django.txt + # edx-opaque-keys pyproject-api==1.6.1 # via tox pytest==7.4.4 @@ -202,7 +210,10 @@ sqlparse==0.4.4 # -r requirements/django.txt # django stevedore==5.1.0 - # via code-annotations + # via + # -r requirements/django.txt + # code-annotations + # edx-opaque-keys text-unidecode==1.3 # via python-slugify tomli==2.0.1 @@ -218,8 +229,10 @@ tox==4.12.1 # via -r requirements/test.in typing-extensions==4.9.0 # via + # -r requirements/django.txt # annotated-types # astroid + # edx-opaque-keys # inflect # pydantic # pydantic-core diff --git a/xblock/__init__.py b/xblock/__init__.py index 564ce3837..d10628e18 100644 --- a/xblock/__init__.py +++ b/xblock/__init__.py @@ -27,4 +27,4 @@ def __init__(self, *args, **kwargs): # without causing a circular import xblock.fields.XBlockMixin = XBlockMixin -__version__ = '1.9.1' +__version__ = '1.10.0' diff --git a/xblock/core.py b/xblock/core.py index 407b5ee68..2c93195ac 100644 --- a/xblock/core.py +++ b/xblock/core.py @@ -205,6 +205,32 @@ def __init__(self, runtime, field_data=None, scope_ids=UNSET, *args, **kwargs): # Provide backwards compatibility for external access through _field_data super().__init__(runtime=runtime, scope_ids=scope_ids, field_data=field_data, *args, **kwargs) + @property + def usage_key(self): + """ + A key identifying this particular usage of the XBlock, unique across all learning contexts in the system. + + Equivalent to to `.scope_ids.usage_id`. + """ + return self.scope_ids.usage_id + + @property + def context_key(self): + """ + A key identifying the learning context (course, library, etc.) that contains this XBlock usage. + + Equivalent to `.scope_ids.usage_id.context_key`. + + Returns: + * `LearningContextKey`, if `.scope_ids.usage_id` is a `UsageKey` instance. + * `None`, otherwise. + + After https://github.com/openedx/XBlock/issues/708 is complete, we can assume that + `.scope_ids.usage_id` is always a `UsageKey`, and that this method will + always return a `LearningContextKey`. + """ + return getattr(self.scope_ids.usage_id, "context_key", None) + def render(self, view, context=None): """Render `view` with this block's runtime and the supplied `context`""" return self.runtime.render(self, view, context) diff --git a/xblock/test/test_core.py b/xblock/test/test_core.py index 9f469850b..7f7cd4700 100644 --- a/xblock/test/test_core.py +++ b/xblock/test/test_core.py @@ -4,6 +4,7 @@ """ # Allow accessing protected members for testing purposes # pylint: disable=protected-access + from datetime import datetime import json import re @@ -12,6 +13,7 @@ import ddt import pytest +from opaque_keys.edx.locator import LibraryUsageLocatorV2, LibraryLocatorV2 from webob import Response from xblock.core import XBlock @@ -1103,3 +1105,42 @@ def test_override_index_view(self): self.assertTrue(index_info) self.assertTrue(isinstance(index_info, dict)) self.assertEqual(index_info["test_field"], "ABC123") + + +class TestScopeIdProperties(unittest.TestCase): + """ + Test the .usage_key and .context_key convenience properties. + """ + + class TestXBlock(XBlock): + pass + + library_key = LibraryLocatorV2(org="myOrg", slug="myLib") + library_block_key = LibraryUsageLocatorV2(library_key, "myType", "myBlock") + + def test_key_properties(self): + scope_ids = ScopeIds( + user_id="myUser", + block_type="myType", + def_id="myDefId", + usage_id=self.library_block_key, + ) + block = XBlock(Mock(spec=Runtime), scope_ids=scope_ids) + self.assertEqual(block.usage_key, self.library_block_key) + self.assertEqual(block.context_key, self.library_key) + + def test_key_properties_when_usage_is_not_an_opaque_key(self): + """ + Tests a legacy scenario that we believe only happens in xblock-sdk at this point. + + Remove this test as part of https://github.com/openedx/XBlock/issues/708. + """ + scope_ids = ScopeIds( + user_id="myUser", + block_type="myType", + def_id="myDefId", + usage_id="myWeirdOldUsageId", + ) + block = XBlock(Mock(spec=Runtime), scope_ids=scope_ids) + self.assertEqual(block.usage_key, "myWeirdOldUsageId") + self.assertIsNone(block.context_key)