Commit c00f80d4 by Calen Pennington

Remove locator.py (now sourced from the external opaque_keys library)

[LMS-2757]
parent b60b7c7e
"""
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(), [])]
"""
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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment