Remove locator.py (now sourced from the external opaque_keys library)
[LMS-2757]
This commit is contained in:
@@ -1,527 +0,0 @@
|
||||
"""
|
||||
Identifier for course resources.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
import inspect
|
||||
import re
|
||||
from abc import abstractmethod
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from bson.errors import InvalidId
|
||||
|
||||
from opaque_keys import OpaqueKey, InvalidKeyError
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey, DefinitionKey
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalId(object):
|
||||
"""
|
||||
Class for local ids for non-persisted xblocks (which can have hardcoded block_ids if necessary)
|
||||
"""
|
||||
def __init__(self, block_id=None):
|
||||
self.block_id = block_id
|
||||
super(LocalId, self).__init__()
|
||||
|
||||
def __str__(self):
|
||||
return "localid_{}".format(self.block_id or id(self))
|
||||
|
||||
|
||||
class Locator(OpaqueKey):
|
||||
"""
|
||||
A locator is like a URL, it refers to a course resource.
|
||||
|
||||
Locator is an abstract base class: do not instantiate
|
||||
"""
|
||||
|
||||
BLOCK_TYPE_PREFIX = r"type"
|
||||
# Prefix for the version portion of a locator URL, when it is preceded by a course ID
|
||||
VERSION_PREFIX = r"version"
|
||||
ALLOWED_ID_CHARS = r'[\w\-~.:]'
|
||||
|
||||
def __str__(self):
|
||||
'''
|
||||
str(self) returns something like this: "mit.eecs.6002x"
|
||||
'''
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
@abstractmethod
|
||||
def version(self):
|
||||
"""
|
||||
Returns the ObjectId referencing this specific location.
|
||||
Raises InvalidKeyError if the instance
|
||||
doesn't have a complete enough specification.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def as_object_id(cls, value):
|
||||
"""
|
||||
Attempts to cast value as a bson.objectid.ObjectId.
|
||||
If cast fails, raises ValueError
|
||||
"""
|
||||
try:
|
||||
return ObjectId(value)
|
||||
except InvalidId:
|
||||
raise ValueError('"%s" is not a valid version_guid' % value)
|
||||
|
||||
|
||||
class BlockLocatorBase(Locator):
|
||||
|
||||
# Token separating org from offering
|
||||
ORG_SEPARATOR = '+'
|
||||
|
||||
# Prefix for the branch portion of a locator URL
|
||||
BRANCH_PREFIX = r"branch"
|
||||
# Prefix for the block portion of a locator URL
|
||||
BLOCK_PREFIX = r"block"
|
||||
|
||||
ALLOWED_ID_RE = re.compile(r'^' + Locator.ALLOWED_ID_CHARS + '+$', re.UNICODE)
|
||||
|
||||
URL_RE_SOURCE = r"""
|
||||
((?P<org>{ALLOWED_ID_CHARS}+)\+(?P<offering>{ALLOWED_ID_CHARS}+)\+?)??
|
||||
({BRANCH_PREFIX}\+(?P<branch>{ALLOWED_ID_CHARS}+)\+?)?
|
||||
({VERSION_PREFIX}\+(?P<version_guid>[A-F0-9]+)\+?)?
|
||||
({BLOCK_TYPE_PREFIX}\+(?P<block_type>{ALLOWED_ID_CHARS}+)\+?)?
|
||||
({BLOCK_PREFIX}\+(?P<block_id>{ALLOWED_ID_CHARS}+))?
|
||||
""".format(
|
||||
ALLOWED_ID_CHARS=Locator.ALLOWED_ID_CHARS, BRANCH_PREFIX=BRANCH_PREFIX,
|
||||
VERSION_PREFIX=Locator.VERSION_PREFIX, BLOCK_TYPE_PREFIX=Locator.BLOCK_TYPE_PREFIX, BLOCK_PREFIX=BLOCK_PREFIX
|
||||
)
|
||||
|
||||
URL_RE = re.compile('^' + URL_RE_SOURCE + '$', re.IGNORECASE | re.VERBOSE | re.UNICODE)
|
||||
|
||||
@classmethod
|
||||
def parse_url(cls, string):
|
||||
"""
|
||||
Raises InvalidKeyError if string cannot be parsed.
|
||||
|
||||
If it can be parsed as a version_guid with no preceding org + offering, returns a dict
|
||||
with key 'version_guid' and the value,
|
||||
|
||||
If it can be parsed as a org + offering, returns a dict
|
||||
with key 'id' and optional keys 'branch' and 'version_guid'.
|
||||
"""
|
||||
match = cls.URL_RE.match(string)
|
||||
if not match:
|
||||
raise InvalidKeyError(cls, string)
|
||||
return match.groupdict()
|
||||
|
||||
|
||||
class CourseLocator(BlockLocatorBase, CourseKey):
|
||||
"""
|
||||
Examples of valid CourseLocator specifications:
|
||||
CourseLocator(version_guid=ObjectId('519665f6223ebd6980884f2b'))
|
||||
CourseLocator(org='mit.eecs', offering='6.002x')
|
||||
CourseLocator(org='mit.eecs', offering='6002x', branch = 'published')
|
||||
CourseLocator.from_string('course-locator:version+519665f6223ebd6980884f2b')
|
||||
CourseLocator.from_string('course-locator:mit.eecs+6002x')
|
||||
CourseLocator.from_string('course-locator:mit.eecs+6002x+branch+published')
|
||||
CourseLocator.from_string('course-locator:mit.eecs+6002x+branch+published+version+519665f6223ebd6980884f2b')
|
||||
|
||||
Should have at least a specific org & offering (id for the course as if it were a project w/
|
||||
versions) with optional 'branch',
|
||||
or version_guid (which points to a specific version). Can contain both in which case
|
||||
the persistence layer may raise exceptions if the given version != the current such version
|
||||
of the course.
|
||||
"""
|
||||
CANONICAL_NAMESPACE = 'course-locator'
|
||||
KEY_FIELDS = ('org', 'offering', 'branch', 'version_guid')
|
||||
|
||||
# stubs to fake out the abstractproperty class instrospection and allow treatment as attrs in instances
|
||||
org = None
|
||||
offering = None
|
||||
|
||||
def __init__(self, org=None, offering=None, branch=None, version_guid=None):
|
||||
"""
|
||||
Construct a CourseLocator
|
||||
|
||||
Args:
|
||||
version_guid (string or ObjectId): optional unique id for the version
|
||||
org, offering (string): the standard definition. Optional only if version_guid given
|
||||
branch (string): the branch such as 'draft', 'published', 'staged', 'beta'
|
||||
"""
|
||||
if version_guid:
|
||||
version_guid = self.as_object_id(version_guid)
|
||||
|
||||
if not all(field is None or self.ALLOWED_ID_RE.match(field) for field in [org, offering, branch]):
|
||||
raise InvalidKeyError(self.__class__, [org, offering, branch])
|
||||
|
||||
super(CourseLocator, self).__init__(
|
||||
org=org,
|
||||
offering=offering,
|
||||
branch=branch,
|
||||
version_guid=version_guid
|
||||
)
|
||||
|
||||
if self.version_guid is None and (self.org is None or self.offering is None):
|
||||
raise InvalidKeyError(self.__class__, "Either version_guid or org and offering should be set")
|
||||
|
||||
def version(self):
|
||||
"""
|
||||
Returns the ObjectId referencing this specific location.
|
||||
"""
|
||||
return self.version_guid
|
||||
|
||||
@classmethod
|
||||
def _from_string(cls, serialized):
|
||||
"""
|
||||
Return a CourseLocator parsing the given serialized string
|
||||
:param serialized: matches the string to a CourseLocator
|
||||
"""
|
||||
parse = cls.parse_url(serialized)
|
||||
|
||||
if parse['version_guid']:
|
||||
parse['version_guid'] = cls.as_object_id(parse['version_guid'])
|
||||
|
||||
return cls(**{key: parse.get(key) for key in cls.KEY_FIELDS})
|
||||
|
||||
def html_id(self):
|
||||
"""
|
||||
Generate a discussion group id based on course
|
||||
|
||||
To make compatible with old Location object functionality. I don't believe this behavior fits at this
|
||||
place, but I have no way to override. We should clearly define the purpose and restrictions of this
|
||||
(e.g., I'm assuming periods are fine).
|
||||
"""
|
||||
return unicode(self)
|
||||
|
||||
def make_usage_key(self, block_type, block_id):
|
||||
return BlockUsageLocator(
|
||||
course_key=self,
|
||||
block_type=block_type,
|
||||
block_id=block_id
|
||||
)
|
||||
|
||||
def make_asset_key(self, asset_type, path):
|
||||
raise NotImplementedError()
|
||||
|
||||
def version_agnostic(self):
|
||||
"""
|
||||
We don't care if the locator's version is not the current head; so, avoid version conflict
|
||||
by reducing info.
|
||||
Returns a copy of itself without any version info.
|
||||
|
||||
:raises: ValueError if the block locator has no org & offering
|
||||
"""
|
||||
return CourseLocator(
|
||||
org=self.org,
|
||||
offering=self.offering,
|
||||
branch=self.branch,
|
||||
version_guid=None
|
||||
)
|
||||
|
||||
def course_agnostic(self):
|
||||
"""
|
||||
We only care about the locator's version not its course.
|
||||
Returns a copy of itself without any course info.
|
||||
|
||||
:raises: ValueError if the block locator has no version_guid
|
||||
"""
|
||||
return CourseLocator(
|
||||
org=None,
|
||||
offering=None,
|
||||
branch=None,
|
||||
version_guid=self.version_guid
|
||||
)
|
||||
|
||||
def for_branch(self, branch):
|
||||
"""
|
||||
Return a new CourseLocator for another branch of the same course (also version agnostic)
|
||||
"""
|
||||
if self.org is None:
|
||||
raise InvalidKeyError(self.__class__, "Branches must have full course ids not just versions")
|
||||
return CourseLocator(
|
||||
org=self.org,
|
||||
offering=self.offering,
|
||||
branch=branch,
|
||||
version_guid=None
|
||||
)
|
||||
|
||||
def for_version(self, version_guid):
|
||||
"""
|
||||
Return a new CourseLocator for another version of the same course and branch. Usually used
|
||||
when the head is updated (and thus the course x branch now points to this version)
|
||||
"""
|
||||
return CourseLocator(
|
||||
org=self.org,
|
||||
offering=self.offering,
|
||||
branch=self.branch,
|
||||
version_guid=version_guid
|
||||
)
|
||||
|
||||
def _to_string(self):
|
||||
"""
|
||||
Return a string representing this location.
|
||||
"""
|
||||
parts = []
|
||||
if self.offering:
|
||||
parts.extend([self.org, self.offering])
|
||||
if self.branch:
|
||||
parts.append(u"{prefix}+{branch}".format(prefix=self.BRANCH_PREFIX, branch=self.branch))
|
||||
if self.version_guid:
|
||||
parts.append(u"{prefix}+{guid}".format(prefix=self.VERSION_PREFIX, guid=self.version_guid))
|
||||
return u"+".join(parts)
|
||||
|
||||
|
||||
class BlockUsageLocator(BlockLocatorBase, UsageKey):
|
||||
"""
|
||||
Encodes a location.
|
||||
|
||||
Locations address modules (aka blocks) which are definitions situated in a
|
||||
course instance. Thus, a Location must identify the course and the occurrence of
|
||||
the defined element in the course. Courses can be a version of an offering, the
|
||||
current draft head, or the current production version.
|
||||
|
||||
Locators can contain both a version and a org + offering w/ branch. The split mongo functions
|
||||
may raise errors if these conflict w/ the current db state (i.e., the course's branch !=
|
||||
the version_guid)
|
||||
|
||||
Locations can express as urls as well as dictionaries. They consist of
|
||||
package_identifier: course_guid | version_guid
|
||||
block : guid
|
||||
branch : string
|
||||
"""
|
||||
CANONICAL_NAMESPACE = 'edx'
|
||||
KEY_FIELDS = ('course_key', 'block_type', 'block_id')
|
||||
|
||||
# fake out class instrospection as this is an attr in this class's instances
|
||||
course_key = None
|
||||
block_type = None
|
||||
|
||||
def __init__(self, course_key, block_type, block_id):
|
||||
"""
|
||||
Construct a BlockUsageLocator
|
||||
"""
|
||||
block_id = self._parse_block_ref(block_id)
|
||||
if block_id is None:
|
||||
raise InvalidKeyError(self.__class__, "Missing block id")
|
||||
|
||||
super(BlockUsageLocator, self).__init__(course_key=course_key, block_type=block_type, block_id=block_id)
|
||||
|
||||
@classmethod
|
||||
def _from_string(cls, serialized):
|
||||
"""
|
||||
Requests CourseLocator to deserialize its part and then adds the local deserialization of block
|
||||
"""
|
||||
course_key = CourseLocator._from_string(serialized)
|
||||
parsed_parts = cls.parse_url(serialized)
|
||||
block_id = parsed_parts.get('block_id', None)
|
||||
if block_id is None:
|
||||
raise InvalidKeyError(cls, serialized)
|
||||
return cls(course_key, parsed_parts.get('block_type'), block_id)
|
||||
|
||||
def version_agnostic(self):
|
||||
"""
|
||||
We don't care if the locator's version is not the current head; so, avoid version conflict
|
||||
by reducing info.
|
||||
Returns a copy of itself without any version info.
|
||||
|
||||
:raises: ValueError if the block locator has no org and offering
|
||||
"""
|
||||
return BlockUsageLocator(
|
||||
course_key=self.course_key.version_agnostic(),
|
||||
block_type=self.block_type,
|
||||
block_id=self.block_id,
|
||||
)
|
||||
|
||||
def course_agnostic(self):
|
||||
"""
|
||||
We only care about the locator's version not its course.
|
||||
Returns a copy of itself without any course info.
|
||||
|
||||
:raises: ValueError if the block locator has no version_guid
|
||||
"""
|
||||
return BlockUsageLocator(
|
||||
course_key=self.course_key.course_agnostic(),
|
||||
block_type=self.block_type,
|
||||
block_id=self.block_id
|
||||
)
|
||||
|
||||
def for_branch(self, branch):
|
||||
"""
|
||||
Return a UsageLocator for the same block in a different branch of the course.
|
||||
"""
|
||||
return BlockUsageLocator(
|
||||
self.course_key.for_branch(branch),
|
||||
block_type=self.block_type,
|
||||
block_id=self.block_id
|
||||
)
|
||||
|
||||
def for_version(self, version_guid):
|
||||
"""
|
||||
Return a UsageLocator for the same block in a different branch of the course.
|
||||
"""
|
||||
return BlockUsageLocator(
|
||||
self.course_key.for_version(version_guid),
|
||||
block_type=self.block_type,
|
||||
block_id=self.block_id
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _parse_block_ref(cls, block_ref):
|
||||
if isinstance(block_ref, LocalId):
|
||||
return block_ref
|
||||
elif len(block_ref) > 0 and cls.ALLOWED_ID_RE.match(block_ref):
|
||||
return block_ref
|
||||
else:
|
||||
raise InvalidKeyError(cls, block_ref)
|
||||
|
||||
@property
|
||||
def definition_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def org(self):
|
||||
return self.course_key.org
|
||||
|
||||
@property
|
||||
def offering(self):
|
||||
return self.course_key.offering
|
||||
|
||||
@property
|
||||
def branch(self):
|
||||
return self.course_key.branch
|
||||
|
||||
@property
|
||||
def version_guid(self):
|
||||
return self.course_key.version_guid
|
||||
|
||||
def version(self):
|
||||
return self.course_key.version_guid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
The ambiguously named field from Location which code expects to find
|
||||
"""
|
||||
return self.block_id
|
||||
|
||||
def is_fully_specified(self):
|
||||
return self.course_key.is_fully_specified()
|
||||
|
||||
@classmethod
|
||||
def make_relative(cls, course_locator, block_type, block_id):
|
||||
"""
|
||||
Return a new instance which has the given block_id in the given course
|
||||
:param course_locator: may be a BlockUsageLocator in the same snapshot
|
||||
"""
|
||||
if hasattr(course_locator, 'course_key'):
|
||||
course_locator = course_locator.course_key
|
||||
return BlockUsageLocator(
|
||||
course_key=course_locator,
|
||||
block_type=block_type,
|
||||
block_id=block_id
|
||||
)
|
||||
|
||||
def map_into_course(self, course_key):
|
||||
"""
|
||||
Return a new instance which has the this block_id in the given course
|
||||
:param course_key: a CourseKey object representing the new course to map into
|
||||
"""
|
||||
return BlockUsageLocator.make_relative(course_key, self.block_type, self.block_id)
|
||||
|
||||
def _to_string(self):
|
||||
"""
|
||||
Return a string representing this location.
|
||||
"""
|
||||
return u"{course_key}+{BLOCK_TYPE_PREFIX}+{block_type}+{BLOCK_PREFIX}+{block_id}".format(
|
||||
course_key=self.course_key._to_string(),
|
||||
BLOCK_TYPE_PREFIX=self.BLOCK_TYPE_PREFIX,
|
||||
block_type=self.block_type,
|
||||
BLOCK_PREFIX=self.BLOCK_PREFIX,
|
||||
block_id=self.block_id
|
||||
)
|
||||
|
||||
def html_id(self):
|
||||
"""
|
||||
Generate a discussion group id based on course
|
||||
|
||||
To make compatible with old Location object functionality. I don't believe this behavior fits at this
|
||||
place, but I have no way to override. We should clearly define the purpose and restrictions of this
|
||||
(e.g., I'm assuming periods are fine).
|
||||
"""
|
||||
return unicode(self)
|
||||
|
||||
|
||||
class DefinitionLocator(Locator, DefinitionKey):
|
||||
"""
|
||||
Container for how to locate a description (the course-independent content).
|
||||
"""
|
||||
CANONICAL_NAMESPACE = 'defx'
|
||||
KEY_FIELDS = ('definition_id', 'block_type')
|
||||
|
||||
# override the abstractproperty
|
||||
block_type = None
|
||||
definition_id = None
|
||||
|
||||
def __init__(self, block_type, definition_id):
|
||||
if isinstance(definition_id, LocalId):
|
||||
super(DefinitionLocator, self).__init__(definition_id=definition_id, block_type=block_type)
|
||||
elif isinstance(definition_id, basestring):
|
||||
try:
|
||||
definition_id = self.as_object_id(definition_id)
|
||||
except ValueError:
|
||||
raise InvalidKeyError(self, definition_id)
|
||||
super(DefinitionLocator, self).__init__(definition_id=definition_id, block_type=block_type)
|
||||
elif isinstance(definition_id, ObjectId):
|
||||
super(DefinitionLocator, self).__init__(definition_id=definition_id, block_type=block_type)
|
||||
|
||||
def _to_string(self):
|
||||
'''
|
||||
Return a string representing this location.
|
||||
unicode(self) returns something like this: "519665f6223ebd6980884f2b+type+problem"
|
||||
'''
|
||||
return u"{}+{}+{}".format(unicode(self.definition_id), self.BLOCK_TYPE_PREFIX, self.block_type)
|
||||
|
||||
URL_RE = re.compile(
|
||||
r"^(?P<definition_id>[A-F0-9]+)\+{}\+(?P<block_type>{ALLOWED_ID_CHARS}+)$".format(
|
||||
Locator.BLOCK_TYPE_PREFIX, ALLOWED_ID_CHARS=Locator.ALLOWED_ID_CHARS
|
||||
),
|
||||
re.IGNORECASE | re.VERBOSE | re.UNICODE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _from_string(cls, serialized):
|
||||
"""
|
||||
Return a DefinitionLocator parsing the given serialized string
|
||||
:param serialized: matches the string to
|
||||
"""
|
||||
parse = cls.URL_RE.match(serialized)
|
||||
if not parse:
|
||||
raise InvalidKeyError(cls, serialized)
|
||||
|
||||
parse = parse.groupdict()
|
||||
if parse['definition_id']:
|
||||
parse['definition_id'] = cls.as_object_id(parse['definition_id'])
|
||||
|
||||
return cls(**{key: parse.get(key) for key in cls.KEY_FIELDS})
|
||||
|
||||
def version(self):
|
||||
"""
|
||||
Returns the ObjectId referencing this specific location.
|
||||
"""
|
||||
return self.definition_id
|
||||
|
||||
|
||||
class VersionTree(object):
|
||||
"""
|
||||
Holds trees of Locators to represent version histories.
|
||||
"""
|
||||
def __init__(self, locator, tree_dict=None):
|
||||
"""
|
||||
:param locator: must be version specific (Course has version_guid or definition had id)
|
||||
"""
|
||||
if not isinstance(locator, Locator) and not inspect.isabstract(locator):
|
||||
raise TypeError("locator {} must be a concrete subclass of Locator".format(locator))
|
||||
if not locator.version():
|
||||
raise ValueError("locator must be version specific (Course has version_guid or definition had id)")
|
||||
self.locator = locator
|
||||
if tree_dict is None:
|
||||
self.children = []
|
||||
else:
|
||||
self.children = [VersionTree(child, tree_dict)
|
||||
for child in tree_dict.get(locator.version(), [])]
|
||||
@@ -1,297 +0,0 @@
|
||||
"""
|
||||
Tests for opaque_keys.edx.locator.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
import random
|
||||
from bson.objectid import ObjectId
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locator import Locator, CourseLocator, BlockUsageLocator, DefinitionLocator
|
||||
from ddt import ddt, data
|
||||
from opaque_keys.edx.keys import UsageKey, CourseKey, DefinitionKey
|
||||
|
||||
|
||||
@ddt
|
||||
class LocatorTest(TestCase):
|
||||
"""
|
||||
Tests for subclasses of Locator.
|
||||
"""
|
||||
|
||||
def test_cant_instantiate_abstract_class(self):
|
||||
self.assertRaises(TypeError, Locator)
|
||||
|
||||
def test_course_constructor_underspecified(self):
|
||||
with self.assertRaises(InvalidKeyError):
|
||||
CourseLocator()
|
||||
with self.assertRaises(InvalidKeyError):
|
||||
CourseLocator(branch='published')
|
||||
|
||||
def test_course_constructor_bad_version_guid(self):
|
||||
with self.assertRaises(ValueError):
|
||||
CourseLocator(version_guid="012345")
|
||||
|
||||
with self.assertRaises(InvalidKeyError):
|
||||
CourseLocator(version_guid=None)
|
||||
|
||||
def test_course_constructor_version_guid(self):
|
||||
# generate a random location
|
||||
test_id_1 = ObjectId()
|
||||
test_id_1_loc = str(test_id_1)
|
||||
testobj_1 = CourseLocator(version_guid=test_id_1)
|
||||
self.check_course_locn_fields(testobj_1, version_guid=test_id_1)
|
||||
self.assertEqual(str(testobj_1.version_guid), test_id_1_loc)
|
||||
self.assertEqual(testobj_1._to_string(), u'+'.join((testobj_1.VERSION_PREFIX, test_id_1_loc)))
|
||||
|
||||
# Test using a given string
|
||||
test_id_2_loc = '519665f6223ebd6980884f2b'
|
||||
test_id_2 = ObjectId(test_id_2_loc)
|
||||
testobj_2 = CourseLocator(version_guid=test_id_2)
|
||||
self.check_course_locn_fields(testobj_2, version_guid=test_id_2)
|
||||
self.assertEqual(str(testobj_2.version_guid), test_id_2_loc)
|
||||
self.assertEqual(testobj_2._to_string(), u'+'.join((testobj_2.VERSION_PREFIX, test_id_2_loc)))
|
||||
|
||||
@data(
|
||||
' mit.eecs',
|
||||
'mit.eecs ',
|
||||
CourseLocator.VERSION_PREFIX + '+mit.eecs',
|
||||
BlockUsageLocator.BLOCK_PREFIX + '+black+mit.eecs',
|
||||
'mit.ee cs',
|
||||
'mit.ee,cs',
|
||||
'mit.ee+cs',
|
||||
'mit.ee&cs',
|
||||
'mit.ee()cs',
|
||||
CourseLocator.BRANCH_PREFIX + '+this',
|
||||
'mit.eecs+' + CourseLocator.BRANCH_PREFIX,
|
||||
'mit.eecs+' + CourseLocator.BRANCH_PREFIX + '+this+' + CourseLocator.BRANCH_PREFIX + '+that',
|
||||
'mit.eecs+' + CourseLocator.BRANCH_PREFIX + '+this+' + CourseLocator.BRANCH_PREFIX,
|
||||
'mit.eecs+' + CourseLocator.BRANCH_PREFIX + '+this ',
|
||||
'mit.eecs+' + CourseLocator.BRANCH_PREFIX + '+th%is ',
|
||||
)
|
||||
def test_course_constructor_bad_package_id(self, bad_id):
|
||||
"""
|
||||
Test all sorts of badly-formed package_ids (and urls with those package_ids)
|
||||
"""
|
||||
with self.assertRaises(InvalidKeyError):
|
||||
CourseLocator(org=bad_id, offering='test')
|
||||
|
||||
with self.assertRaises(InvalidKeyError):
|
||||
CourseLocator(org='test', offering=bad_id)
|
||||
|
||||
with self.assertRaises(InvalidKeyError):
|
||||
CourseKey.from_string('course-locator:test+{}'.format(bad_id))
|
||||
|
||||
@data('course-locator:', 'course-locator:/mit.eecs', 'http:mit.eecs', 'course-locator//mit.eecs')
|
||||
def test_course_constructor_bad_url(self, bad_url):
|
||||
with self.assertRaises(InvalidKeyError):
|
||||
CourseKey.from_string(bad_url)
|
||||
|
||||
def test_course_constructor_url(self):
|
||||
# Test parsing a url when it starts with a version ID and there is also a block ID.
|
||||
# This hits the parsers parse_guid method.
|
||||
test_id_loc = '519665f6223ebd6980884f2b'
|
||||
testobj = CourseKey.from_string("course-locator:{}+{}+{}+hw3".format(
|
||||
CourseLocator.VERSION_PREFIX, test_id_loc, CourseLocator.BLOCK_PREFIX
|
||||
))
|
||||
self.check_course_locn_fields(
|
||||
testobj,
|
||||
version_guid=ObjectId(test_id_loc)
|
||||
)
|
||||
|
||||
def test_course_constructor_url_package_id_and_version_guid(self):
|
||||
test_id_loc = '519665f6223ebd6980884f2b'
|
||||
testobj = CourseKey.from_string(
|
||||
'course-locator:mit.eecs+honors.6002x+{}+{}'.format(CourseLocator.VERSION_PREFIX, test_id_loc)
|
||||
)
|
||||
self.check_course_locn_fields(
|
||||
testobj,
|
||||
org='mit.eecs',
|
||||
offering='honors.6002x',
|
||||
version_guid=ObjectId(test_id_loc)
|
||||
)
|
||||
|
||||
def test_course_constructor_url_package_id_branch_and_version_guid(self):
|
||||
test_id_loc = '519665f6223ebd6980884f2b'
|
||||
org = 'mit.eecs'
|
||||
offering = '~6002x'
|
||||
testobj = CourseKey.from_string('course-locator:{}+{}+{}+draft-1+{}+{}'.format(
|
||||
org, offering, CourseLocator.BRANCH_PREFIX, CourseLocator.VERSION_PREFIX, test_id_loc
|
||||
))
|
||||
self.check_course_locn_fields(
|
||||
testobj,
|
||||
org=org,
|
||||
offering=offering,
|
||||
branch='draft-1',
|
||||
version_guid=ObjectId(test_id_loc)
|
||||
)
|
||||
|
||||
def test_course_constructor_package_id_no_branch(self):
|
||||
org = 'mit.eecs'
|
||||
offering = '6002x'
|
||||
testurn = '{}+{}'.format(org, offering)
|
||||
testobj = CourseLocator(org=org, offering=offering)
|
||||
self.check_course_locn_fields(testobj, org=org, offering=offering)
|
||||
self.assertEqual(testobj._to_string(), testurn)
|
||||
|
||||
def test_course_constructor_package_id_separate_branch(self):
|
||||
org = 'mit.eecs'
|
||||
offering = '6002x'
|
||||
test_branch = 'published'
|
||||
expected_urn = '{}+{}+{}+{}'.format(org, offering, CourseLocator.BRANCH_PREFIX, test_branch)
|
||||
testobj = CourseLocator(org=org, offering=offering, branch=test_branch)
|
||||
self.check_course_locn_fields(
|
||||
testobj,
|
||||
org=org,
|
||||
offering=offering,
|
||||
branch=test_branch,
|
||||
)
|
||||
self.assertEqual(testobj.branch, test_branch)
|
||||
self.assertEqual(testobj._to_string(), expected_urn)
|
||||
|
||||
def test_block_constructor(self):
|
||||
expected_org = 'mit.eecs'
|
||||
expected_offering = '6002x'
|
||||
expected_branch = 'published'
|
||||
expected_block_ref = 'HW3'
|
||||
testurn = 'edx:{}+{}+{}+{}+{}+{}+{}+{}'.format(
|
||||
expected_org, expected_offering, CourseLocator.BRANCH_PREFIX, expected_branch,
|
||||
BlockUsageLocator.BLOCK_TYPE_PREFIX, 'problem', BlockUsageLocator.BLOCK_PREFIX, 'HW3'
|
||||
)
|
||||
testobj = UsageKey.from_string(testurn)
|
||||
self.check_block_locn_fields(
|
||||
testobj,
|
||||
org=expected_org,
|
||||
offering=expected_offering,
|
||||
branch=expected_branch,
|
||||
block_type='problem',
|
||||
block=expected_block_ref
|
||||
)
|
||||
self.assertEqual(unicode(testobj), testurn)
|
||||
testobj = testobj.for_version(ObjectId())
|
||||
agnostic = testobj.version_agnostic()
|
||||
self.assertIsNone(agnostic.version_guid)
|
||||
self.check_block_locn_fields(agnostic,
|
||||
org=expected_org,
|
||||
offering=expected_offering,
|
||||
branch=expected_branch,
|
||||
block=expected_block_ref)
|
||||
|
||||
def test_block_constructor_url_version_prefix(self):
|
||||
test_id_loc = '519665f6223ebd6980884f2b'
|
||||
testobj = UsageKey.from_string(
|
||||
'edx:mit.eecs+6002x+{}+{}+{}+problem+{}+lab2'.format(
|
||||
CourseLocator.VERSION_PREFIX, test_id_loc, BlockUsageLocator.BLOCK_TYPE_PREFIX, BlockUsageLocator.BLOCK_PREFIX
|
||||
)
|
||||
)
|
||||
self.check_block_locn_fields(
|
||||
testobj,
|
||||
org='mit.eecs',
|
||||
offering='6002x',
|
||||
block_type='problem',
|
||||
block='lab2',
|
||||
version_guid=ObjectId(test_id_loc)
|
||||
)
|
||||
agnostic = testobj.course_agnostic()
|
||||
self.check_block_locn_fields(
|
||||
agnostic,
|
||||
block='lab2',
|
||||
org=None,
|
||||
offering=None,
|
||||
version_guid=ObjectId(test_id_loc)
|
||||
)
|
||||
self.assertIsNone(agnostic.offering)
|
||||
self.assertIsNone(agnostic.org)
|
||||
|
||||
def test_block_constructor_url_kitchen_sink(self):
|
||||
test_id_loc = '519665f6223ebd6980884f2b'
|
||||
testobj = UsageKey.from_string(
|
||||
'edx:mit.eecs+6002x+{}+draft+{}+{}+{}+problem+{}+lab2'.format(
|
||||
CourseLocator.BRANCH_PREFIX, CourseLocator.VERSION_PREFIX, test_id_loc,
|
||||
BlockUsageLocator.BLOCK_TYPE_PREFIX, BlockUsageLocator.BLOCK_PREFIX
|
||||
)
|
||||
)
|
||||
self.check_block_locn_fields(
|
||||
testobj,
|
||||
org='mit.eecs',
|
||||
offering='6002x',
|
||||
branch='draft',
|
||||
block='lab2',
|
||||
version_guid=ObjectId(test_id_loc)
|
||||
)
|
||||
|
||||
def test_colon_name(self):
|
||||
"""
|
||||
It seems we used to use colons in names; so, ensure they're acceptable.
|
||||
"""
|
||||
org = 'mit.eecs'
|
||||
offering = '1'
|
||||
branch = 'foo'
|
||||
block_id = 'problem:with-colon~2'
|
||||
testobj = BlockUsageLocator(
|
||||
CourseLocator(org=org, offering=offering, branch=branch),
|
||||
block_type='problem',
|
||||
block_id=block_id
|
||||
)
|
||||
self.check_block_locn_fields(
|
||||
testobj, org=org, offering=offering, branch=branch, block=block_id
|
||||
)
|
||||
|
||||
def test_relative(self):
|
||||
"""
|
||||
Test making a relative usage locator.
|
||||
"""
|
||||
org = 'mit.eecs'
|
||||
offering = '1'
|
||||
branch = 'foo'
|
||||
baseobj = CourseLocator(org=org, offering=offering, branch=branch)
|
||||
block_id = 'problem:with-colon~2'
|
||||
testobj = BlockUsageLocator.make_relative(baseobj, 'problem', block_id)
|
||||
self.check_block_locn_fields(
|
||||
testobj, org=org, offering=offering, branch=branch, block=block_id
|
||||
)
|
||||
block_id = 'completely_different'
|
||||
testobj = BlockUsageLocator.make_relative(testobj, 'problem', block_id)
|
||||
self.check_block_locn_fields(
|
||||
testobj, org=org, offering=offering, branch=branch, block=block_id
|
||||
)
|
||||
|
||||
def test_repr(self):
|
||||
testurn = u'edx:mit.eecs+6002x+{}+published+{}+problem+{}+HW3'.format(
|
||||
CourseLocator.BRANCH_PREFIX, BlockUsageLocator.BLOCK_TYPE_PREFIX, BlockUsageLocator.BLOCK_PREFIX
|
||||
)
|
||||
testobj = UsageKey.from_string(testurn)
|
||||
self.assertEqual("BlockUsageLocator(CourseLocator(u'mit.eecs', u'6002x', u'published', None), u'problem', u'HW3')", repr(testobj))
|
||||
|
||||
def test_description_locator_url(self):
|
||||
object_id = '{:024x}'.format(random.randrange(16 ** 24))
|
||||
definition_locator = DefinitionLocator('html', object_id)
|
||||
self.assertEqual('defx:{}+{}+html'.format(object_id, DefinitionLocator.BLOCK_TYPE_PREFIX), unicode(definition_locator))
|
||||
self.assertEqual(definition_locator, DefinitionKey.from_string(unicode(definition_locator)))
|
||||
|
||||
def test_description_locator_version(self):
|
||||
object_id = '{:024x}'.format(random.randrange(16 ** 24))
|
||||
definition_locator = DefinitionLocator('html', object_id)
|
||||
self.assertEqual(object_id, str(definition_locator.version()))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Utilities
|
||||
|
||||
def check_course_locn_fields(self, testobj, version_guid=None,
|
||||
org=None, offering=None, branch=None):
|
||||
"""
|
||||
Checks the version, org, offering, and branch in testobj
|
||||
"""
|
||||
self.assertEqual(testobj.version_guid, version_guid)
|
||||
self.assertEqual(testobj.org, org)
|
||||
self.assertEqual(testobj.offering, offering)
|
||||
self.assertEqual(testobj.branch, branch)
|
||||
|
||||
def check_block_locn_fields(self, testobj, version_guid=None,
|
||||
org=None, offering=None, branch=None, block_type=None, block=None):
|
||||
"""
|
||||
Does adds a block id check over and above the check_course_locn_fields tests
|
||||
"""
|
||||
self.check_course_locn_fields(testobj, version_guid, org, offering,
|
||||
branch)
|
||||
if block_type is not None:
|
||||
self.assertEqual(testobj.block_type, block_type)
|
||||
self.assertEqual(testobj.block_id, block)
|
||||
Reference in New Issue
Block a user