Commit ded28af8 by Don Mitchell

Make Locators comply with UsageKey accessors

parent 6d7fe561
......@@ -12,7 +12,6 @@ from xmodule.html_module import HtmlDescriptor
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@unittest.skip("Not fixing split until we land opaque-keys 0.9")
class TemplateTests(unittest.TestCase):
"""
Test finding and using the templates (boilerplates) for xblocks.
......@@ -55,25 +54,25 @@ class TemplateTests(unittest.TestCase):
def test_factories(self):
test_course = persistent_factories.PersistentCourseFactory.create(
course_id='testx.tempcourse', org='testx',
offering='tempcourse', org='testx',
display_name='fun test course', user_id='testbot'
)
self.assertIsInstance(test_course, CourseDescriptor)
self.assertEqual(test_course.display_name, 'fun test course')
index_info = modulestore('split').get_course_index_info(test_course.location)
index_info = modulestore('split').get_course_index_info(test_course.id)
self.assertEqual(index_info['org'], 'testx')
self.assertEqual(index_info['_id'], 'testx.tempcourse')
self.assertEqual(index_info['offering'], 'tempcourse')
test_chapter = persistent_factories.ItemFactory.create(display_name='chapter 1',
parent_location=test_course.location)
self.assertIsInstance(test_chapter, SequenceDescriptor)
# refetch parent which should now point to child
test_course = modulestore('split').get_course(test_course.id)
test_course = modulestore('split').get_course(test_course.id.version_agnostic())
self.assertIn(test_chapter.location.block_id, test_course.children)
with self.assertRaises(DuplicateCourseError):
persistent_factories.PersistentCourseFactory.create(
course_id='testx.tempcourse', org='testx',
offering='tempcourse', org='testx',
display_name='fun test course', user_id='testbot'
)
......@@ -82,7 +81,7 @@ class TemplateTests(unittest.TestCase):
Test create_xblock to create non persisted xblocks
"""
test_course = persistent_factories.PersistentCourseFactory.create(
course_id='testx.tempcourse', org='testx',
offering='tempcourse', org='testx',
display_name='fun test course', user_id='testbot'
)
......@@ -109,7 +108,7 @@ class TemplateTests(unittest.TestCase):
try saving temporary xblocks
"""
test_course = persistent_factories.PersistentCourseFactory.create(
course_id='testx.tempcourse', org='testx',
offering='tempcourse', org='testx',
display_name='fun test course', user_id='testbot'
)
test_chapter = modulestore('split').create_xblock(
......@@ -148,7 +147,7 @@ class TemplateTests(unittest.TestCase):
def test_delete_course(self):
test_course = persistent_factories.PersistentCourseFactory.create(
course_id='edu.harvard.history.doomed', org='testx',
offering='history.doomed', org='edu.harvard',
display_name='doomed test course',
user_id='testbot')
persistent_factories.ItemFactory.create(display_name='chapter 1',
......@@ -171,7 +170,7 @@ class TemplateTests(unittest.TestCase):
Test get_block_generations
"""
test_course = persistent_factories.PersistentCourseFactory.create(
course_id='edu.harvard.history.hist101', org='testx',
offering='history.hist101', org='edu.harvard',
display_name='history test course',
user_id='testbot'
)
......@@ -193,7 +192,9 @@ class TemplateTests(unittest.TestCase):
second_problem = persistent_factories.ItemFactory.create(
display_name='problem 2',
parent_location=BlockUsageLocator.make_relative(updated_loc, block_id=sub.location.block_id),
parent_location=BlockUsageLocator.make_relative(
updated_loc, block_type='problem', block_id=sub.location.block_id
),
user_id='testbot', category='problem',
data="<problem></problem>"
)
......
......@@ -7,7 +7,7 @@ import lxml
from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import parsers
from xmodule.modulestore.locator import Locator
class TestCourseIndex(CourseTestCase):
......@@ -38,7 +38,7 @@ class TestCourseIndex(CourseTestCase):
for link in course_link_eles:
self.assertRegexpMatches(
link.get("href"),
'course/slashes:{0}'.format(parsers.ALLOWED_ID_CHARS)
'course/slashes:{0}'.format(Locator.ALLOWED_ID_CHARS)
)
# now test that url
outline_response = authed_client.get(link.get("href"), {}, HTTP_ACCEPT='text/html')
......
......@@ -79,5 +79,8 @@ setup(
'asset-location = xmodule.modulestore.locations:AssetLocation',
'edx = xmodule.modulestore.locator:BlockUsageLocator',
],
'definition_key': [
'defx = xmodule.modulestore.locator:DefinitionLocator',
],
},
)
......@@ -58,7 +58,7 @@ class DefinitionKey(OpaqueKey):
KEY_TYPE = 'definition_key'
__slots__ = ()
@abstractmethod
@abstractproperty
def block_type(self):
"""
The XBlock type of this definition.
......@@ -125,6 +125,10 @@ class UsageKey(CourseObjectMixin, OpaqueKey):
"""
raise NotImplementedError()
@property
def block_type(self):
return self.category
class OpaqueKeyReader(IdReader):
"""
......
......@@ -150,6 +150,7 @@ class LocMapperStore(object):
entry = self._migrate_if_necessary([entry])[0]
block_id = entry['block_map'].get(self.encode_key_for_mongo(location.name))
category = location.category
if block_id is None:
if add_entry_if_missing:
block_id = self._add_to_block_map(
......@@ -159,14 +160,15 @@ class LocMapperStore(object):
raise ItemNotFoundError(location)
else:
# jump_to_id uses a None category.
if location.category is None:
if category is None:
if len(block_id) == 1:
# unique match (most common case)
category = block_id.keys()[0]
block_id = block_id.values()[0]
else:
raise InvalidLocationError()
elif location.category in block_id:
block_id = block_id[location.category]
elif category in block_id:
block_id = block_id[category]
elif add_entry_if_missing:
block_id = self._add_to_block_map(location, course_son, entry['block_map'])
else:
......@@ -179,10 +181,12 @@ class LocMapperStore(object):
)
published_usage = BlockUsageLocator(
prod_course_locator,
block_type=category,
block_id=block_id
)
draft_usage = BlockUsageLocator(
prod_course_locator.for_branch(entry['draft_branch']),
block_type=category,
block_id=block_id
)
if published:
......@@ -285,6 +289,7 @@ class LocMapperStore(object):
org=entry[entry_org], offering=entry[entry_offering],
branch=entry['prod_branch']
),
block_type=category,
block_id=block_id
)
draft_locator = BlockUsageLocator(
......@@ -292,6 +297,7 @@ class LocMapperStore(object):
org=entry[entry_org], offering=entry[entry_offering],
branch=entry['draft_branch']
),
block_type=category,
block_id=block_id
)
self._cache_location_map_entry(location, published_locator, draft_locator)
......
......@@ -13,15 +13,7 @@ from bson.errors import InvalidId
from opaque_keys import OpaqueKey, InvalidKeyError
from xmodule.modulestore.keys import CourseKey, UsageKey
from xmodule.modulestore.parsers import (
parse_url,
parse_block_ref,
BRANCH_PREFIX,
BLOCK_PREFIX,
VERSION_PREFIX,
ALLOWED_ID_RE)
from xmodule.modulestore.keys import CourseKey, UsageKey, DefinitionKey
log = logging.getLogger(__name__)
......@@ -45,14 +37,10 @@ class Locator(OpaqueKey):
Locator is an abstract base class: do not instantiate
"""
@abstractmethod
def url(self):
"""
Return a string containing the URL for this location. Raises
InvalidKeyError if the instance doesn't have a
complete enough specification to generate a url
"""
raise NotImplementedError()
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):
'''
......@@ -86,35 +74,42 @@ class BlockLocatorBase(Locator):
# Token separating org from offering
ORG_SEPARATOR = '+'
def version(self):
"""
Returns the ObjectId referencing this specific location.
"""
return self.version_guid
# 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)
def url(self):
"""
Return a string containing the URL for this location.
"""
return self.NAMESPACE_SEPARATOR.join([self.CANONICAL_NAMESPACE, self._to_string()])
@classmethod
def _parse_url(cls, url):
"""
url must be a string beginning with 'edx:' and containing
either a valid version_guid or org & offering (with optional branch), or both.
def parse_url(cls, string):
"""
if not isinstance(url, basestring):
raise TypeError('%s is not an instance of basestring' % url)
Raises InvalidKeyError if string cannot be parsed.
parse = parse_url(url)
if not parse:
raise InvalidKeyError(cls, url)
if parse['version_guid']:
parse['version_guid'] = cls.as_object_id(parse['version_guid'])
If it can be parsed as a version_guid with no preceding org + offering, returns a dict
with key 'version_guid' and the value,
return parse
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()
@property
def package_id(self):
......@@ -130,13 +125,10 @@ class CourseLocator(BlockLocatorBase, CourseKey):
CourseLocator(version_guid=ObjectId('519665f6223ebd6980884f2b'))
CourseLocator(org='mit.eecs', offering='6.002x')
CourseLocator(org='mit.eecs', offering='6002x', branch = 'published')
CourseLocator.from_string('edx:version/519665f6223ebd6980884f2b')
CourseLocator.from_string('version/519665f6223ebd6980884f2b')
CourseLocator.from_string('edx:mit.eecs+6002x')
CourseLocator.from_string('mit.eecs+6002x')
CourseLocator.from_string('edx:mit.eecs+6002x/branch/published')
CourseLocator.from_string('edx:mit.eecs+6002x/branch/published/version/519665f6223ebd6980884f2b')
CourseLocator.from_string('mit.eecs+6002x/branch/published/version/519665f6223ebd6980884f2b')
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',
......@@ -163,7 +155,7 @@ class CourseLocator(BlockLocatorBase, CourseKey):
if version_guid:
version_guid = self.as_object_id(version_guid)
if not all(field is None or ALLOWED_ID_RE.match(field) for field in [org, offering, branch]):
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__(
......@@ -173,31 +165,27 @@ class CourseLocator(BlockLocatorBase, CourseKey):
version_guid=version_guid
)
if self.version_guid is None and self.org is None and self.offering is None:
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
"""
kwargs = cls._parse_url(serialized)
try:
return cls(**{key: kwargs.get(key) for key in cls.KEY_FIELDS})
except ValueError:
raise InvalidKeyError(cls, "Either version_guid or org and offering should be set: {}".format(serialized))
parse = cls.parse_url(serialized)
def is_fully_specified(self):
"""
Returns True if either version_guid is specified, or org+offering+branch
are specified.
This should always return True, since this should be validated in the constructor.
"""
return (
self.version_guid is not None or
(self.org is not None and self.offering is not None and self.branch is not None)
)
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):
"""
......@@ -212,6 +200,7 @@ class CourseLocator(BlockLocatorBase, CourseKey):
def make_usage_key(self, block_type, block_id):
return BlockUsageLocator(
course_key=self,
block_type=block_type,
block_id=block_id
)
......@@ -280,13 +269,13 @@ class CourseLocator(BlockLocatorBase, CourseKey):
if self.offering:
parts.append(unicode(self.package_id))
if self.branch:
parts.append(u"{prefix}{branch}".format(prefix=BRANCH_PREFIX, branch=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=VERSION_PREFIX, guid=self.version_guid))
return u"/".join(parts)
parts.append(u"{prefix}+{guid}".format(prefix=self.VERSION_PREFIX, guid=self.version_guid))
return u"+".join(parts)
class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey methods
class BlockUsageLocator(BlockLocatorBase, UsageKey):
"""
Encodes a location.
......@@ -305,12 +294,13 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
branch : string
"""
CANONICAL_NAMESPACE = 'edx'
KEY_FIELDS = ('course_key', 'block_id')
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_id):
def __init__(self, course_key, block_type, block_id):
"""
Construct a BlockUsageLocator
"""
......@@ -318,7 +308,7 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
if block_id is None:
raise InvalidKeyError(self.__class__, "Missing block id")
super(BlockUsageLocator, self).__init__(course_key=course_key, block_id=block_id)
super(BlockUsageLocator, self).__init__(course_key=course_key, block_type=block_type, block_id=block_id)
@classmethod
def _from_string(cls, serialized):
......@@ -326,11 +316,11 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
Requests CourseLocator to deserialize its part and then adds the local deserialization of block
"""
course_key = CourseLocator._from_string(serialized)
parsed_parts = parse_url(serialized)
block_id = parsed_parts.get('block_id')
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, block_id)
return cls(course_key, parsed_parts.get('block_type'), block_id)
def version_agnostic(self):
"""
......@@ -342,7 +332,8 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
"""
return BlockUsageLocator(
course_key=self.course_key.version_agnostic(),
block_id=self.block_id
block_type=self.block_type,
block_id=self.block_id,
)
def course_agnostic(self):
......@@ -354,6 +345,7 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
"""
return BlockUsageLocator(
course_key=self.course_key.course_agnostic(),
block_type=self.block_type,
block_id=self.block_id
)
......@@ -363,6 +355,17 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
"""
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
)
......@@ -370,11 +373,10 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
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:
parse = parse_block_ref(block_ref)
if not parse:
raise InvalidKeyError(cls, block_ref)
return parse.get('block_id')
raise InvalidKeyError(cls, block_ref)
@property
def definition_key(self):
......@@ -400,6 +402,9 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
def version_guid(self):
return self.course_key.version_guid
def version(self):
return self.course_key.version_guid
@property
def name(self):
"""
......@@ -411,7 +416,7 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
return self.course_key.is_fully_specified()
@classmethod
def make_relative(cls, course_locator, block_id):
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
......@@ -420,6 +425,7 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
course_locator = course_locator.course_key
return BlockUsageLocator(
course_key=course_locator,
block_type=block_type,
block_id=block_id
)
......@@ -428,34 +434,17 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
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_id)
def url_reverse(self, prefix, postfix=''):
"""
Do what reverse is supposed to do but seems unable to do. Generate a url using prefix unicode(self) postfix
:param prefix: the beginning of the url (will be forced to begin and end with / if non-empty)
:param postfix: the part to append to the url (will be forced to begin w/ / if non-empty)
"""
if prefix:
if not prefix.endswith('/'):
prefix += '/'
if not prefix.startswith('/'):
prefix = '/' + prefix
else:
prefix = '/'
if postfix and not postfix.startswith('/'):
postfix = '/' + postfix
elif postfix is None:
postfix = ''
return prefix + unicode(self) + postfix
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_PREFIX}{block_id}".format(
return u"{course_key}+{BLOCK_TYPE_PREFIX}+{block_type}+{BLOCK_PREFIX}+{block_id}".format(
course_key=self.course_key._to_string(),
BLOCK_PREFIX=BLOCK_PREFIX,
BLOCK_TYPE_PREFIX=self.BLOCK_TYPE_PREFIX,
block_type=self.block_type,
BLOCK_PREFIX=self.BLOCK_PREFIX,
block_id=self.block_id
)
......@@ -467,43 +456,61 @@ class BlockUsageLocator(BlockLocatorBase, UsageKey): # TODO implement UsageKey
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 re.sub('[^\w-]', '-', self._to_string())
return unicode(self)
class DefinitionLocator(Locator):
class DefinitionLocator(Locator, DefinitionKey):
"""
Container for how to locate a description (the course-independent content).
"""
CANONICAL_NAMESPACE = 'defx'
KEY_FIELDS = ('definition_id',)
KEY_FIELDS = ('definition_id', 'block_type')
URL_RE = re.compile(r'^defx:' + VERSION_PREFIX + '([^/]+)$', re.IGNORECASE)
# override the abstractproperty
block_type = None
definition_id = None
def __init__(self, definition_id):
def __init__(self, block_type, definition_id):
if isinstance(definition_id, LocalId):
super(DefinitionLocator, self).__init__(definition_id)
super(DefinitionLocator, self).__init__(definition_id=definition_id, block_type=block_type)
elif isinstance(definition_id, basestring):
regex_match = self.URL_RE.match(definition_id)
if regex_match is not None:
super(DefinitionLocator, self).__init__(self.as_object_id(regex_match.group(1)))
else:
super(DefinitionLocator, self).__init__(self.as_object_id(definition_id))
else:
super(DefinitionLocator, self).__init__(self.as_object_id(definition_id))
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: "version/519665f6223ebd6980884f2b"
unicode(self) returns something like this: "519665f6223ebd6980884f2b+type+problem"
'''
return VERSION_PREFIX + str(self.definition_id)
return u"{}+{}+{}".format(unicode(self.definition_id), self.BLOCK_TYPE_PREFIX, self.block_type)
def url(self):
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 string containing the URL for this location.
url(self) returns something like this: 'defx:version/519665f6223ebd6980884f2b'
Return a DefinitionLocator parsing the given serialized string
:param serialized: matches the string to
"""
return u'defx:' + self._to_string()
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):
"""
......
import re
# 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/"
# 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\-~.:+]'
ALLOWED_ID_RE = re.compile(r'^{}+$'.format(ALLOWED_ID_CHARS), re.UNICODE)
# NOTE: if we need to support period in place of +, make it aggressive (take the first period in the string)
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_PREFIX}(?P<block_id>{ALLOWED_ID_CHARS}+))?
""".format(
ALLOWED_ID_CHARS=ALLOWED_ID_CHARS, BRANCH_PREFIX=BRANCH_PREFIX,
VERSION_PREFIX=VERSION_PREFIX, BLOCK_PREFIX=BLOCK_PREFIX
)
URL_RE = re.compile('^' + URL_RE_SOURCE + '$', re.IGNORECASE | re.VERBOSE | re.UNICODE)
def parse_url(string):
"""
followed by either a version_guid or a org + offering pair. If tag_optional, then
the url does not have to start with the tag and edx will be assumed.
Examples:
'edx:version/0123FFFF'
'edx:mit.eecs.6002x'
'edx:mit.eecs.6002x/branch/published'
'edx:mit.eecs.6002x/branch/published/block/HW3'
'edx:mit.eecs.6002x/branch/published/version/000eee12345/block/HW3'
This returns None 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 = URL_RE.match(string)
if not match:
return None
matched_dict = match.groupdict()
return matched_dict
def parse_block_ref(string):
r"""
A block_ref is a string of url safe characters (see ALLOWED_ID_CHARS)
If string is a block_ref, returns a dict with key 'block_ref' and the value,
otherwise returns None.
"""
if ALLOWED_ID_RE.match(string):
return {'block_id': string}
return None
......@@ -108,6 +108,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
offering=course_entry_override.get('offering'),
branch=course_entry_override.get('branch'),
),
block_type=json_data.get('category'),
block_id=block_id,
)
......@@ -131,6 +132,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self,
BlockUsageLocator(
CourseLocator(version_guid=course_entry_override['structure']['_id']),
block_type='error',
block_id=block_id
),
error_msg=exc_info_to_str(sys.exc_info())
......
......@@ -8,14 +8,14 @@ class DefinitionLazyLoader(object):
object doesn't force access during init but waits until client wants the
definition. Only works if the modulestore is a split mongo store.
"""
def __init__(self, modulestore, definition_id):
def __init__(self, modulestore, block_type, definition_id):
"""
Simple placeholder for yet-to-be-fetched data
:param modulestore: the pymongo db connection with the definitions
:param definition_locator: the id of the record in the above to fetch
"""
self.modulestore = modulestore
self.definition_locator = DefinitionLocator(definition_id)
self.definition_locator = DefinitionLocator(block_type, definition_id)
def fetch(self):
"""
......
......@@ -152,7 +152,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if lazy:
for block in new_module_data.itervalues():
block['definition'] = DefinitionLazyLoader(self, block['definition'])
block['definition'] = DefinitionLazyLoader(self, block['category'], block['definition'])
else:
# Load all descendants by id
descendent_definitions = self.db_connection.find_matching_definitions({
......@@ -242,11 +242,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
:param course_locator: any subclass of CourseLocator
'''
# NOTE: if and when this uses cache, the update if changed logic will break if the cache
# holds the same objects as the descriptors!
if not course_locator.is_fully_specified():
raise InsufficientSpecificationError('Not fully specified: %s' % course_locator)
if course_locator.org and course_locator.offering and course_locator.branch:
# use the course id
index = self.db_connection.get_course_index(course_locator)
......@@ -258,6 +253,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if course_locator.version_guid is not None and version_guid != course_locator.version_guid:
# This may be a bit too touchy but it's hard to infer intent
raise VersionConflictError(course_locator, version_guid)
elif course_locator.version_guid is None:
raise InsufficientSpecificationError(course_locator)
else:
# TODO should this raise an exception if branch was provided?
version_guid = course_locator.version_guid
......@@ -322,9 +319,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
def get_course(self, course_id, depth=None):
'''
Gets the course descriptor for the course identified by the locator
which may or may not be a blockLocator.
raises InsufficientSpecificationError
'''
assert(isinstance(course_id, CourseLocator))
course_entry = self._lookup_course(course_id)
......@@ -458,6 +452,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
return [
BlockUsageLocator.make_relative(
locator,
block_type=course['structure']['blocks'][parent_id].get('category'),
block_id=LocMapperStore.decode_key_from_mongo(parent_id),
)
for parent_id in items
......@@ -471,12 +466,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
course = self._lookup_course(course_key)
items = {LocMapperStore.decode_key_from_mongo(block_id) for block_id in course['structure']['blocks'].keys()}
items.remove(course['structure']['root'])
for block_id, block_data in course['structure']['blocks'].iteritems():
blocks = course['structure']['blocks']
for block_id, block_data in blocks.iteritems():
items.difference_update(block_data.get('fields', {}).get('children', []))
if block_data['category'] in detached_categories:
items.discard(LocMapperStore.decode_key_from_mongo(block_id))
return [
BlockUsageLocator(course_key=course_key, block_id=block_id)
BlockUsageLocator(course_key=course_key, block_type=blocks[block_id]['category'], block_id=block_id)
for block_id in items
]
......@@ -613,11 +609,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# convert the results value sets to locators
for k, versions in result.iteritems():
result[k] = [
BlockUsageLocator(CourseLocator(version_guid=version), block_id=block_id)
block_locator.for_version(version)
for version in versions
]
return VersionTree(
BlockUsageLocator(CourseLocator(version_guid=possible_roots[0]), block_id=block_id),
block_locator.for_version(possible_roots[0]),
result
)
......@@ -650,7 +646,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
'schema_version': self.SCHEMA_VERSION,
}
self.db_connection.insert_definition(document)
definition_locator = DefinitionLocator(new_id)
definition_locator = DefinitionLocator(category, new_id)
return definition_locator
def update_definition_from_data(self, definition_locator, new_def_data, user_id):
......@@ -685,7 +681,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
old_definition['edit_info']['previous_version'] = definition_locator.definition_id
old_definition['schema_version'] = self.SCHEMA_VERSION
self.db_connection.insert_definition(old_definition)
return DefinitionLocator(old_definition['_id']), True
return DefinitionLocator(old_definition['category'], old_definition['_id']), True
else:
return definition_locator, False
......@@ -829,11 +825,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
self._update_head(index_entry, course_or_parent_locator.branch, new_id)
item_loc = BlockUsageLocator(
course_or_parent_locator.version_agnostic(),
block_type=category,
block_id=new_block_id,
)
else:
item_loc = BlockUsageLocator(
CourseLocator(version_guid=new_id),
block_type=category,
block_id=new_block_id,
)
......@@ -1029,7 +1027,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
course_key = CourseLocator(version_guid=new_id)
# fetch and return the new item--fetching is unnecessary but a good qc step
new_locator = BlockUsageLocator(course_key, descriptor.location.block_id)
new_locator = descriptor.location.map_into_course(course_key)
return self.get_item(new_locator)
else:
# nothing changed, just return the one sent in
......@@ -1101,18 +1099,14 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
self._update_head(index_entry, xblock.location.branch, new_id)
# fetch and return the new item--fetching is unnecessary but a good qc step
return self.get_item(
BlockUsageLocator(
xblock.location.course_key.for_version(new_id),
block_id=xblock.location.block_id,
)
)
return self.get_item(xblock.location.for_version(new_id))
else:
return xblock
def _persist_subdag(self, xblock, user_id, structure_blocks, new_id):
# persist the definition if persisted != passed
new_def_data = self._filter_special_fields(xblock.get_explicitly_set_fields_by_scope(Scope.content))
is_updated = False
if xblock.definition_locator is None or isinstance(xblock.definition_locator.definition_id, LocalId):
xblock.definition_locator = self.create_definition_from_data(
new_def_data, xblock.category, user_id)
......@@ -1134,9 +1128,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
else:
is_new = False
encoded_block_id = LocMapperStore.encode_key_for_mongo(xblock.location.block_id)
is_updated = is_updated or (
xblock.has_children and structure_blocks[encoded_block_id]['fields']['children'] != xblock.children
)
children = []
if xblock.has_children:
......@@ -1147,6 +1138,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
children.append(child_block.location.block_id)
else:
children.append(child)
is_updated = is_updated or structure_blocks[encoded_block_id]['fields']['children'] != children
block_fields = xblock.get_explicitly_set_fields_by_scope(Scope.settings)
if not is_new and not is_updated:
......@@ -1419,9 +1411,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if isinstance(definition, DefinitionLazyLoader):
return definition.definition_locator
elif '_id' not in definition:
return DefinitionLocator(LocalId())
return DefinitionLocator(definition.get('category'), LocalId())
else:
return DefinitionLocator(definition['_id'])
return DefinitionLocator(definition['category'], definition['_id'])
def get_modulestore_type(self, course_id):
"""
......
......@@ -32,14 +32,14 @@ class PersistentCourseFactory(SplitFactory):
# pylint: disable=W0613
@classmethod
def _create(cls, target_class, course_id='testX.999', org='testX', user_id='test_user',
def _create(cls, target_class, offering='999', org='testX', user_id='test_user',
master_branch='draft', **kwargs):
modulestore = kwargs.pop('modulestore')
root_block_id = kwargs.pop('root_block_id', 'course')
# Write the data to the mongo datastore
new_course = modulestore.create_course(
course_id, org, user_id, fields=kwargs,
org, offering, user_id, fields=kwargs,
master_branch=master_branch, root_block_id=root_block_id
)
......
......@@ -267,6 +267,7 @@ class TestLocationMapper(LocMapperSetupSansDjango):
)
prob_locator = BlockUsageLocator(
prob_course_key,
block_type='problem',
block_id='problem2',
)
prob_location = loc_mapper().translate_locator_to_location(prob_locator)
......@@ -289,20 +290,21 @@ class TestLocationMapper(LocMapperSetupSansDjango):
prob_location = loc_mapper().translate_locator_to_location(prob_locator, get_course=True)
self.assertEqual(prob_location, SlashSeparatedCourseKey(org, course, run))
# explicit branch
prob_locator = BlockUsageLocator(
prob_course_key.for_branch('draft'), block_id=prob_locator.block_id
)
prob_locator = prob_locator.for_branch('draft')
prob_location = loc_mapper().translate_locator_to_location(prob_locator)
# Even though the problem was set as draft, we always return revision=None to work
# with old mongo/draft modulestores.
self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None))
prob_locator = BlockUsageLocator(prob_course_key.for_branch('production'), block_id='problem2')
prob_locator = BlockUsageLocator(
prob_course_key.for_branch('production'),
block_type='problem', block_id='problem2'
)
prob_location = loc_mapper().translate_locator_to_location(prob_locator)
self.assertEqual(prob_location, Location(org, course, run, 'problem', 'abc123', None))
# same for chapter except chapter cannot be draft in old system
chap_locator = BlockUsageLocator(
prob_course_key.for_branch('production'),
block_id='chapter48f',
block_type='chapter', block_id='chapter48f',
)
chap_location = loc_mapper().translate_locator_to_location(chap_locator)
self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234'))
......@@ -311,7 +313,7 @@ class TestLocationMapper(LocMapperSetupSansDjango):
chap_location = loc_mapper().translate_locator_to_location(chap_locator)
self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234'))
chap_locator = BlockUsageLocator(
prob_course_key.for_branch('production'), block_id='chapter48f'
prob_course_key.for_branch('production'), block_type='chapter', block_id='chapter48f'
)
chap_location = loc_mapper().translate_locator_to_location(chap_locator)
self.assertEqual(chap_location, Location(org, course, run, 'chapter', '48f23a10395384929234'))
......@@ -319,7 +321,7 @@ class TestLocationMapper(LocMapperSetupSansDjango):
# look for non-existent problem
prob_locator2 = BlockUsageLocator(
prob_course_key.for_branch('draft'),
block_id='problem3'
block_type='problem', block_id='problem3'
)
prob_location = loc_mapper().translate_locator_to_location(prob_locator2)
self.assertIsNone(prob_location, 'Found non-existent problem')
......
......@@ -7,8 +7,8 @@ import random
from bson.objectid import ObjectId
from opaque_keys import InvalidKeyError
from xmodule.modulestore.locator import Locator, CourseLocator, BlockUsageLocator, DefinitionLocator
from xmodule.modulestore.parsers import BRANCH_PREFIX, BLOCK_PREFIX, VERSION_PREFIX
from ddt import ddt, data
from xmodule.modulestore.keys import UsageKey, CourseKey, DefinitionKey
@ddt
......@@ -40,7 +40,7 @@ class LocatorTest(TestCase):
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(), VERSION_PREFIX + 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'
......@@ -48,24 +48,24 @@ class LocatorTest(TestCase):
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(), VERSION_PREFIX + 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 ',
VERSION_PREFIX + 'mit.eecs',
BLOCK_PREFIX + 'black/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',
'mit.ee()cs',
BRANCH_PREFIX + 'this',
'mit.eecs/' + BRANCH_PREFIX,
'mit.eecs/' + BRANCH_PREFIX + 'this/' + BRANCH_PREFIX + 'that',
'mit.eecs/' + BRANCH_PREFIX + 'this/' + BRANCH_PREFIX,
'mit.eecs/' + BRANCH_PREFIX + 'this ',
'mit.eecs/' + BRANCH_PREFIX + 'th%is ',
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):
"""
......@@ -78,18 +78,20 @@ class LocatorTest(TestCase):
CourseLocator(org='test', offering=bad_id)
with self.assertRaises(InvalidKeyError):
CourseLocator.from_string('course-locator:' + bad_id)
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):
CourseLocator.from_string(bad_url)
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 = CourseLocator.from_string("course-locator:{}{}/{}hw3".format(VERSION_PREFIX, test_id_loc, BLOCK_PREFIX))
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)
......@@ -97,7 +99,9 @@ class LocatorTest(TestCase):
def test_course_constructor_url_package_id_and_version_guid(self):
test_id_loc = '519665f6223ebd6980884f2b'
testobj = CourseLocator.from_string('course-locator:mit.eecs+honors.6002x/' + VERSION_PREFIX + test_id_loc)
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',
......@@ -109,8 +113,8 @@ class LocatorTest(TestCase):
test_id_loc = '519665f6223ebd6980884f2b'
org = 'mit.eecs'
offering = '~6002x'
testobj = CourseLocator.from_string('course-locator:{}+{}/{}draft-1/{}{}'.format(
org, offering, BRANCH_PREFIX, VERSION_PREFIX, test_id_loc
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,
......@@ -126,15 +130,13 @@ class LocatorTest(TestCase):
testurn = '{}+{}'.format(org, offering)
testobj = CourseLocator(org=org, offering=offering)
self.check_course_locn_fields(testobj, org=org, offering=offering)
self.assertEqual(testobj.package_id, testurn)
self.assertEqual(testobj._to_string(), testurn)
def test_course_constructor_package_id_separate_branch(self):
org = 'mit.eecs'
offering = '6002x'
testurn = '{}+{}'.format(org, offering)
test_branch = 'published'
expected_urn = '{}+{}/{}{}'.format(org, offering, BRANCH_PREFIX, test_branch)
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,
......@@ -142,7 +144,6 @@ class LocatorTest(TestCase):
offering=offering,
branch=test_branch,
)
self.assertEqual(testobj.package_id, testurn)
self.assertEqual(testobj.branch, test_branch)
self.assertEqual(testobj._to_string(), expected_urn)
......@@ -151,17 +152,21 @@ class LocatorTest(TestCase):
expected_offering = '6002x'
expected_branch = 'published'
expected_block_ref = 'HW3'
testurn = 'edx:{}+{}/{}{}/{}{}'.format(
expected_org, expected_offering, BRANCH_PREFIX, expected_branch, BLOCK_PREFIX, '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
)
testobj = BlockUsageLocator.from_string(testurn)
self.check_block_locn_fields(testobj,
org=expected_org,
offering=expected_offering,
branch=expected_branch,
block=expected_block_ref)
self.assertEqual(unicode(testobj), testurn)
testobj = BlockUsageLocator(testobj.course_key.for_version(ObjectId()), testobj.block_id)
testobj = testobj.for_version(ObjectId())
agnostic = testobj.version_agnostic()
self.assertIsNone(agnostic.version_guid)
self.check_block_locn_fields(agnostic,
......@@ -172,13 +177,16 @@ class LocatorTest(TestCase):
def test_block_constructor_url_version_prefix(self):
test_id_loc = '519665f6223ebd6980884f2b'
testobj = BlockUsageLocator.from_string(
'edx:mit.eecs+6002x/{}{}/{}lab2'.format(VERSION_PREFIX, test_id_loc, BLOCK_PREFIX)
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)
)
......@@ -195,9 +203,10 @@ class LocatorTest(TestCase):
def test_block_constructor_url_kitchen_sink(self):
test_id_loc = '519665f6223ebd6980884f2b'
testobj = BlockUsageLocator.from_string(
'edx:mit.eecs+6002x/{}draft/{}{}/{}lab2'.format(
BRANCH_PREFIX, VERSION_PREFIX, test_id_loc, BLOCK_PREFIX
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(
......@@ -219,6 +228,7 @@ class LocatorTest(TestCase):
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(
......@@ -234,54 +244,32 @@ class LocatorTest(TestCase):
branch = 'foo'
baseobj = CourseLocator(org=org, offering=offering, branch=branch)
block_id = 'problem:with-colon~2'
testobj = BlockUsageLocator.make_relative(baseobj, block_id)
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, block_id)
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 = 'edx:mit.eecs+6002x/' + BRANCH_PREFIX + 'published/' + BLOCK_PREFIX + 'HW3'
testobj = BlockUsageLocator.from_string(testurn)
self.assertEqual("BlockUsageLocator(CourseLocator(u'mit.eecs', u'6002x', u'published', None), u'HW3')", repr(testobj))
def test_url_reverse(self):
"""
Test the url_reverse method
"""
locator = BlockUsageLocator(
CourseLocator(org="a", offering="fancy_course-id", branch="branch_1.2-3"),
block_id='element'
)
self.assertEqual(
'/expression/{}/format'.format(unicode(locator)),
locator.url_reverse('expression', 'format')
)
self.assertEqual(
'/expression/{}/format'.format(unicode(locator)),
locator.url_reverse('/expression', '/format')
)
self.assertEqual(
'/expression/{}'.format(unicode(locator)),
locator.url_reverse('expression/', None)
)
self.assertEqual(
'/expression/{}'.format(unicode(locator)),
locator.url_reverse('/expression/', '')
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(object_id)
self.assertEqual('defx:' + VERSION_PREFIX + object_id, unicode(definition_locator))
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(object_id)
definition_locator = DefinitionLocator('html', object_id)
self.assertEqual(object_id, str(definition_locator.version()))
# ------------------------------------------------------------------
......@@ -298,10 +286,12 @@ class LocatorTest(TestCase):
self.assertEqual(testobj.branch, branch)
def check_block_locn_fields(self, testobj, version_guid=None,
org=None, offering=None, branch=None, block=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)
......@@ -442,7 +442,7 @@ class SplitModuleTest(unittest.TestCase):
Sets up the initial data into the db
'''
split_store = modulestore()
for course_id, course_spec in SplitModuleTest.COURSE_CONTENT.iteritems():
for _course_id, course_spec in SplitModuleTest.COURSE_CONTENT.iteritems():
course = split_store.create_course(
course_spec['org'], course_spec['offering'], course_spec['user_id'],
fields=course_spec['fields'],
......@@ -454,7 +454,8 @@ class SplitModuleTest(unittest.TestCase):
if course.location.block_id == block_id:
block = course
else:
block_usage = BlockUsageLocator.make_relative(course.location, block_id)
# not easy to figure out the category but get_item won't care
block_usage = BlockUsageLocator.make_relative(course.location, '', block_id)
block = split_store.get_item(block_usage)
for key, value in fields.iteritems():
setattr(block, key, value)
......@@ -466,7 +467,7 @@ class SplitModuleTest(unittest.TestCase):
elif spec['parent'] == course.location.block_id:
parent = course
else:
block_usage = BlockUsageLocator.make_relative(course.location, spec['parent'])
block_usage = BlockUsageLocator.make_relative(course.location, '', spec['parent'])
parent = split_store.get_item(block_usage)
block_id = LocalId(spec['id'])
child = split_store.create_xblock(
......@@ -477,6 +478,7 @@ class SplitModuleTest(unittest.TestCase):
# publish "testx.wonderful"
to_publish = BlockUsageLocator(
CourseLocator(org="testx", offering="wonderful", branch="draft"),
block_type='course',
block_id="head23456"
)
destination = CourseLocator(org="testx", offering="wonderful", branch="published")
......@@ -676,12 +678,12 @@ class SplitModuleItemTests(SplitModuleTest):
course = modulestore().get_course(course_locator)
previous_version = course.previous_version
# positive tests of various forms
locator = BlockUsageLocator(CourseLocator(version_guid=previous_version), block_id='head12345')
locator = course.location.map_into_course(CourseLocator(version_guid=previous_version))
self.assertTrue(
modulestore().has_item(locator), "couldn't find in %s" % previous_version
)
locator = BlockUsageLocator(course_locator, block_id='head12345')
locator = course.location.version_agnostic()
self.assertTrue(
modulestore().has_item(locator),
)
......@@ -689,6 +691,7 @@ class SplitModuleItemTests(SplitModuleTest):
modulestore().has_item(
BlockUsageLocator(
locator.course_key.for_branch('published'),
block_type=locator.block_type,
block_id=locator.block_id
)
),
......@@ -696,7 +699,7 @@ class SplitModuleItemTests(SplitModuleTest):
)
# not a course obj
locator = BlockUsageLocator(course_locator, block_id='chapter1')
locator = BlockUsageLocator(course_locator, block_type='chapter', block_id='chapter1')
self.assertTrue(
modulestore().has_item(locator),
"couldn't find chapter1"
......@@ -705,26 +708,25 @@ class SplitModuleItemTests(SplitModuleTest):
# in published course
locator = BlockUsageLocator(
CourseLocator(org="testx", offering="wonderful", branch='draft'),
block_type="course",
block_id="head23456"
)
self.assertTrue(
modulestore().has_item(
BlockUsageLocator(locator.course_key.for_branch("published"), block_id=locator.block_id)
)
modulestore().has_item(locator.for_branch("published"))
)
locator = locator.for_branch('published')
self.assertTrue(modulestore().has_item(locator), "couldn't find in published")
def test_negative_has_item(self):
# negative tests--not found
# no such course or block
locator = BlockUsageLocator(
CourseLocator(org="foo", offering="doesnotexist", branch='draft'),
block_type="course",
block_id="head23456"
)
self.assertFalse(modulestore().has_item(locator))
locator = BlockUsageLocator(
CourseLocator(org="testx", offering="wonderful", branch='draft'),
block_type="vertical",
block_id="doesnotexist"
)
self.assertFalse(modulestore().has_item(locator))
......@@ -738,7 +740,7 @@ class SplitModuleItemTests(SplitModuleTest):
previous_version = course.previous_version
# positive tests of various forms
locator = BlockUsageLocator(CourseLocator(version_guid=previous_version), block_id='head12345')
locator = course.location.map_into_course(CourseLocator(version_guid=previous_version))
block = modulestore().get_item(locator)
self.assertIsInstance(block, CourseDescriptor)
self.assertIsInstance(modulestore().get_item(locator), CourseDescriptor)
......@@ -759,36 +761,27 @@ class SplitModuleItemTests(SplitModuleTest):
block.grade_cutoffs, {"Pass": 0.45},
)
locator = BlockUsageLocator(hero_locator, block_id='head12345')
verify_greek_hero(modulestore().get_item(locator))
verify_greek_hero(modulestore().get_item(course.location))
# try to look up other branches
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(
BlockUsageLocator(
hero_locator.for_branch("published"),
block_id=locator.block_id,
)
)
self.assertIsInstance(
modulestore().get_item(locator),
CourseDescriptor
)
modulestore().get_item(course.location.for_branch("published"))
def test_get_non_root(self):
# not a course obj
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'chapter1'
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'chapter', 'chapter1'
)
block = modulestore().get_item(locator)
self.assertEqual(block.location.package_id, "testx+GreekHero")
self.assertEqual(block.location.org, "testx")
self.assertEqual(block.location.offering, "GreekHero")
self.assertEqual(block.category, 'chapter')
self.assertEqual(block.display_name, "Hercules")
self.assertEqual(block.edited_by, "testassist@edx.org")
# in published course
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='wonderful', branch='published'), 'head23456'
CourseLocator(org='testx', offering='wonderful', branch='published'), 'course', 'head23456'
)
self.assertIsInstance(
modulestore().get_item(locator),
......@@ -798,12 +791,12 @@ class SplitModuleItemTests(SplitModuleTest):
# negative tests--not found
# no such course or block
locator = BlockUsageLocator(
CourseLocator(org='doesnotexist', offering='doesnotexist', branch='draft'), 'head23456'
CourseLocator(org='doesnotexist', offering='doesnotexist', branch='draft'), 'course', 'head23456'
)
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(locator)
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='wonderful', branch='draft'), 'doesnotexist'
CourseLocator(org='testx', offering='wonderful', branch='draft'), 'html', 'doesnotexist'
)
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(locator)
......@@ -864,7 +857,7 @@ class SplitModuleItemTests(SplitModuleTest):
'''
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'),
block_id='chapter1'
'chapter', block_id='chapter1'
)
parents = modulestore().get_parent_locations(locator)
self.assertEqual(len(parents), 1)
......@@ -884,7 +877,7 @@ class SplitModuleItemTests(SplitModuleTest):
Test the existing get_children method on xdescriptors
"""
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'head12345'
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'course', 'head12345'
)
block = modulestore().get_item(locator)
children = block.get_children()
......@@ -952,11 +945,11 @@ class TestItemCrud(SplitModuleTest):
self.assertIsNotNone(new_module.definition_locator)
self.assertEqual(new_module.display_name, 'new sequential')
# check that block does not exist in previous version
locator = BlockUsageLocator(
CourseLocator(version_guid=premod_course.location.version_guid),
block_id=new_module.location.block_id
locator = new_module.location.map_into_course(
CourseLocator(version_guid=premod_course.location.version_guid)
)
self.assertRaises(ItemNotFoundError, modulestore().get_item, locator)
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(locator)
def test_create_parented_item(self):
"""
......@@ -964,12 +957,12 @@ class TestItemCrud(SplitModuleTest):
"""
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'),
block_id='chapter2'
'chapter', block_id='chapter2'
)
original = modulestore().get_item(locator)
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='wonderful', branch='draft'), 'head23456'
CourseLocator(org='testx', offering='wonderful', branch='draft'), 'course', 'head23456'
)
premod_course = modulestore().get_course(locator.course_key)
category = 'chapter'
......@@ -992,12 +985,12 @@ class TestItemCrud(SplitModuleTest):
"""
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'),
block_id='problem1'
'problem', block_id='problem1'
)
original = modulestore().get_item(locator)
locator = BlockUsageLocator(
CourseLocator(org='guestx', offering='contender', branch='draft'), 'head345679'
CourseLocator(org='guestx', offering='contender', branch='draft'), 'course', 'head345679'
)
category = 'problem'
new_payload = "<problem>empty</problem>"
......@@ -1031,8 +1024,8 @@ class TestItemCrud(SplitModuleTest):
Check that using odd characters in block id don't break ability to add and retrieve block.
"""
course_key = CourseLocator(org='guestx', offering='contender', branch='draft')
parent_locator = BlockUsageLocator(course_key, block_id="head345679")
chapter_locator = BlockUsageLocator(course_key, block_id="foo.bar_-~:0")
parent_locator = BlockUsageLocator(course_key, 'course', block_id="head345679")
chapter_locator = BlockUsageLocator(course_key, 'chapter', block_id="foo.bar_-~:0")
modulestore().create_item(
parent_locator, 'chapter', 'anotheruser',
block_id=chapter_locator.block_id,
......@@ -1043,7 +1036,7 @@ class TestItemCrud(SplitModuleTest):
self.assertEqual(new_module.location.block_id, "foo.bar_-~:0") # hardcode to ensure BUL init didn't change
# now try making that a parent of something
new_payload = "<problem>empty</problem>"
problem_locator = BlockUsageLocator(course_key, block_id="prob.bar_-~:99a")
problem_locator = BlockUsageLocator(course_key, 'problem', block_id="prob.bar_-~:99a")
modulestore().create_item(
chapter_locator, 'problem', 'anotheruser',
block_id=problem_locator.block_id,
......@@ -1119,10 +1112,7 @@ class TestItemCrud(SplitModuleTest):
)
# add new child to old parent in continued (leave off version_guid)
course_module_locator = BlockUsageLocator(
new_course.location.course_key.version_agnostic(),
block_id=new_course.location.block_id,
)
course_module_locator = new_course.location.version_agnostic()
new_ele = modulestore().create_item(
course_module_locator, 'chapter', user,
fields={'display_name': 'chapter 4'},
......@@ -1143,7 +1133,7 @@ class TestItemCrud(SplitModuleTest):
"""
locator = BlockUsageLocator(
CourseLocator(org="testx", offering="GreekHero", branch='draft'),
block_id="problem3_2"
'problem', block_id="problem3_2"
)
problem = modulestore().get_item(locator)
pre_def_id = problem.definition_locator.definition_id
......@@ -1160,10 +1150,7 @@ class TestItemCrud(SplitModuleTest):
self.assertNotEqual(updated_problem.location.version_guid, pre_version_guid)
self.assertEqual(updated_problem.max_attempts, 4)
# refetch to ensure original didn't change
original_location = BlockUsageLocator(
CourseLocator(version_guid=pre_version_guid),
block_id=problem.location.block_id
)
original_location = problem.location.map_into_course(CourseLocator(version_guid=pre_version_guid))
problem = modulestore().get_item(original_location)
self.assertNotEqual(problem.max_attempts, 4, "original changed")
......@@ -1179,7 +1166,7 @@ class TestItemCrud(SplitModuleTest):
test updating an item's children ensuring the definition doesn't version but the course does if it should
"""
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'chapter3'
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'chapter', 'chapter3'
)
block = modulestore().get_item(locator)
pre_def_id = block.definition_locator.definition_id
......@@ -1206,7 +1193,7 @@ class TestItemCrud(SplitModuleTest):
test updating an item's definition: ensure it gets versioned as well as the course getting versioned
"""
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'head12345'
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'course', 'head12345'
)
block = modulestore().get_item(locator)
pre_def_id = block.definition_locator.definition_id
......@@ -1226,13 +1213,13 @@ class TestItemCrud(SplitModuleTest):
"""
locator = BlockUsageLocator(
CourseLocator('testx', 'GreekHero', branch='draft'),
block_id='problem1'
'problem', block_id='problem1'
)
original = modulestore().get_item(locator)
# first add 2 children to the course for the update to manipulate
locator = BlockUsageLocator(
CourseLocator('guestx', 'contender', branch='draft'),
block_id="head345679"
'course', block_id="head345679"
)
category = 'problem'
new_payload = "<problem>empty</problem>"
......@@ -1282,11 +1269,7 @@ class TestItemCrud(SplitModuleTest):
with self.assertRaises(VersionConflictError):
modulestore().has_item(locn_to_del)
locator = BlockUsageLocator(
CourseLocator(version_guid=locn_to_del.version_guid),
block_id=locn_to_del.block_id
)
self.assertTrue(modulestore().has_item(locator))
self.assertTrue(modulestore().has_item(locn_to_del.course_agnostic()))
self.assertNotEqual(new_course_loc.version_guid, course.location.version_guid)
# delete a subtree
......@@ -1301,22 +1284,9 @@ class TestItemCrud(SplitModuleTest):
if node:
node_loc = node.location
self.assertFalse(
modulestore().has_item(
BlockUsageLocator(
CourseLocator(
org=node_loc.org,
offering=node_loc.offering,
branch=node_loc.branch,
),
block_id=node_loc.block_id
)
)
modulestore().has_item(node_loc.version_agnostic())
)
locator = BlockUsageLocator(
CourseLocator(version_guid=node.location.version_guid),
block_id=node.location.block_id
)
self.assertTrue(modulestore().has_item(locator))
self.assertTrue(modulestore().has_item(node_loc.course_agnostic()))
if node.has_children:
for sub in node.get_children():
check_subtree(sub)
......@@ -1327,10 +1297,7 @@ class TestItemCrud(SplitModuleTest):
Create a course we can delete
"""
course = modulestore().create_course('nihilx', 'deletion', 'deleting_user')
root = BlockUsageLocator(
course.id.version_agnostic().for_branch('draft'),
block_id=course.location.block_id,
)
root = course.location.version_agnostic().for_branch('draft')
for _ in range(4):
self.create_subtree_for_deletion(root, ['chapter', 'vertical', 'problem'])
return modulestore().get_item(root)
......@@ -1342,7 +1309,7 @@ class TestItemCrud(SplitModuleTest):
if not category_queue:
return
node = modulestore().create_item(parent.version_agnostic(), category_queue[0], 'deleting_user')
node_loc = BlockUsageLocator(parent.course_key, block_id=node.location.block_id)
node_loc = node.location.map_into_course(parent.course_key)
for _ in range(4):
self.create_subtree_for_deletion(node_loc, category_queue[1:])
......@@ -1523,13 +1490,13 @@ class TestInheritance(SplitModuleTest):
# Note, not testing value where defined (course) b/c there's no
# defined accessor for it on CourseDescriptor.
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'problem3_2'
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'problem', 'problem3_2'
)
node = modulestore().get_item(locator)
# inherited
self.assertEqual(node.graceperiod, datetime.timedelta(hours=2))
locator = BlockUsageLocator(
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'problem1'
CourseLocator(org='testx', offering='GreekHero', branch='draft'), 'problem', 'problem1'
)
node = modulestore().get_item(locator)
# overridden
......@@ -1560,19 +1527,19 @@ class TestPublish(SplitModuleTest):
)
# add a child under chapter1
new_module = modulestore().create_item(
BlockUsageLocator.make_relative(source_course, "chapter1"), "sequential", self.user,
BlockUsageLocator.make_relative(source_course, "chapter", "chapter1"), "sequential", self.user,
fields={'display_name': 'new sequential'},
)
# remove chapter1 from expected b/c its pub'd version != the source anymore since source changed
expected.remove("chapter1")
# check that it's not in published course
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(BlockUsageLocator.make_relative(dest_course, new_module.location.block_id))
modulestore().get_item(new_module.location.map_into_course(dest_course))
# publish it
modulestore().xblock_publish(self.user, source_course, dest_course, [new_module.location.block_id], None)
expected.append(new_module.location.block_id)
# check that it is in the published course and that its parent is the chapter
pub_module = modulestore().get_item(BlockUsageLocator.make_relative(dest_course, new_module.location.block_id))
pub_module = modulestore().get_item(new_module.location.map_into_course(dest_course))
self.assertEqual(
modulestore().get_parent_locations(pub_module.location)[0].block_id, "chapter1"
)
......@@ -1584,7 +1551,7 @@ class TestPublish(SplitModuleTest):
modulestore().xblock_publish(self.user, source_course, dest_course, [new_module.location.block_id], None)
expected.append(new_module.location.block_id)
# check that it is in the published course (no error means it worked)
pub_module = modulestore().get_item(BlockUsageLocator.make_relative(dest_course, new_module.location.block_id))
pub_module = modulestore().get_item(new_module.location.map_into_course(dest_course))
self._check_course(
source_course, dest_course, expected, ["chapter2", "chapter3", "problem1", "problem3_2"]
)
......@@ -1617,11 +1584,11 @@ class TestPublish(SplitModuleTest):
expected = ["head12345", "chapter1", "chapter3", "problem1", "problem3_2"]
self._check_course(source_course, dest_course, expected, ["chapter2"])
# now move problem1 and delete problem3_2
chapter1 = modulestore().get_item(BlockUsageLocator.make_relative(source_course, "chapter1"))
chapter3 = modulestore().get_item(BlockUsageLocator.make_relative(source_course, "chapter3"))
chapter1 = modulestore().get_item(source_course.make_usage_key("chapter", "chapter1"))
chapter3 = modulestore().get_item(source_course.make_usage_key("chapter", "chapter3"))
chapter1.children.append("problem1")
chapter3.children.remove("problem1")
modulestore().delete_item(BlockUsageLocator.make_relative(source_course, "problem3_2"), self.user)
modulestore().delete_item(source_course.make_usage_key("problem", "problem3_2"), self.user)
modulestore().xblock_publish(self.user, source_course, dest_course, ["head12345"], ["chapter2"])
expected = ["head12345", "chapter1", "chapter3", "problem1"]
self._check_course(source_course, dest_course, expected, ["chapter2", "problem3_2"])
......@@ -1633,8 +1600,9 @@ class TestPublish(SplitModuleTest):
history_info = modulestore().get_course_history_info(dest_course_loc)
self.assertEqual(history_info['edited_by'], self.user)
for expected in expected_blocks:
source = modulestore().get_item(BlockUsageLocator.make_relative(source_course_loc, expected))
pub_copy = modulestore().get_item(BlockUsageLocator.make_relative(dest_course_loc, expected))
# since block_type has no impact on identity, we can just provide an empty string
source = modulestore().get_item(source_course_loc.make_usage_key("", expected))
pub_copy = modulestore().get_item(dest_course_loc.make_usage_key("", expected))
# everything except previous_version & children should be the same
self.assertEqual(source.category, pub_copy.category)
self.assertEqual(source.update_version, pub_copy.update_version)
......@@ -1649,7 +1617,7 @@ class TestPublish(SplitModuleTest):
self.assertEqual(field.read_from(source), field.read_from(pub_copy))
for unexp in unexpected_blocks:
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(BlockUsageLocator.make_relative(dest_course_loc, unexp))
modulestore().get_item(dest_course_loc.make_usage_key("", unexp))
def _compare_children(self, source_children, dest_children, unexpected):
"""
......
......@@ -105,6 +105,7 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
# create pointer for split
course_or_parent_locator = BlockUsageLocator(
course_key=self.split_course_key,
block_type=parent_category,
block_id=parent_name
)
else:
......
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