Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 38 additions & 37 deletions common/lib/xmodule/xmodule/modulestore/locator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""
Created on Mar 13, 2013

@author: dmitchell
Identifier for course resources.
"""

from __future__ import absolute_import
import logging
import inspect
Expand All @@ -15,6 +14,7 @@
from xmodule.modulestore.exceptions import InsufficientSpecificationError, OverSpecificationError

from .parsers import parse_url, parse_course_id, parse_block_ref
from .parsers import BRANCH_PREFIX, BLOCK_PREFIX, URL_VERSION_PREFIX

log = logging.getLogger(__name__)

Expand All @@ -37,9 +37,6 @@ def url(self):
"""
raise InsufficientSpecificationError()

def quoted_url(self):
return quote(self.url(), '@;#')

def __eq__(self, other):
return self.__dict__ == other.__dict__

Expand Down Expand Up @@ -90,11 +87,12 @@ class CourseLocator(Locator):
Examples of valid CourseLocator specifications:
CourseLocator(version_guid=ObjectId('519665f6223ebd6980884f2b'))
CourseLocator(course_id='mit.eecs.6002x')
CourseLocator(course_id='mit.eecs.6002x;published')
CourseLocator(course_id='mit.eecs.6002x/branch/published')
CourseLocator(course_id='mit.eecs.6002x', branch='published')
CourseLocator(url='edx://@519665f6223ebd6980884f2b')
CourseLocator(url='edx://version/519665f6223ebd6980884f2b')
CourseLocator(url='edx://mit.eecs.6002x')
CourseLocator(url='edx://mit.eecs.6002x;published')
CourseLocator(url='edx://mit.eecs.6002x/branch/published')
CourseLocator(url='edx://mit.eecs.6002x/branch/published/version/519665f6223ebd6980884f2b')

Should have at lease a specific course_id (id for the course as if it were a project w/
versions) with optional 'branch',
Expand All @@ -115,10 +113,10 @@ def __unicode__(self):
if self.course_id:
result = self.course_id
if self.branch:
result += ';' + self.branch
result += BRANCH_PREFIX + self.branch
return result
elif self.version_guid:
return '@' + str(self.version_guid)
return URL_VERSION_PREFIX + str(self.version_guid)
else:
# raise InsufficientSpecificationError("missing course_id or version_guid")
return '<InsufficientSpecificationError: missing course_id or version_guid>'
Expand Down Expand Up @@ -223,21 +221,18 @@ def as_object_id(cls, value):
def init_from_url(self, url):
"""
url must be a string beginning with 'edx://' and containing
either a valid version_guid or course_id (with optional branch)
If a block ('#HW3') is present, it is ignored.
either a valid version_guid or course_id (with optional branch), or both.
"""
if isinstance(url, Locator):
url = url.url()
assert isinstance(url, basestring), \
'%s is not an instance of basestring' % url
assert isinstance(url, basestring), '%s is not an instance of basestring' % url
parse = parse_url(url)
assert parse, 'Could not parse "%s" as a url' % url
if 'version_guid' in parse:
new_guid = parse['version_guid']
self.set_version_guid(self.as_object_id(new_guid))
else:
self.set_course_id(parse['id'])
self.set_branch(parse['branch'])
self._set_value(
parse, 'version_guid', lambda (new_guid): self.set_version_guid(self.as_object_id(new_guid))
)
self._set_value(parse, 'id', lambda (new_id): self.set_course_id(new_id))
self._set_value(parse, 'branch', lambda (new_branch): self.set_branch(new_branch))

def init_from_version_guid(self, version_guid):
"""
Expand All @@ -253,14 +248,14 @@ def init_from_version_guid(self, version_guid):

def init_from_course_id(self, course_id, explicit_branch=None):
"""
Course_id is a string like 'mit.eecs.6002x' or 'mit.eecs.6002x;published'.
Course_id is a string like 'mit.eecs.6002x' or 'mit.eecs.6002x/branch/published'.

Revision (optional) is a string like 'published'.
It may be provided explicitly (explicit_branch) or embedded into course_id.
If branch is part of course_id ("...;published"), parse it out separately.
If branch is part of course_id (".../branch/published"), parse it out separately.
If branch is provided both ways, that's ok as long as they are the same value.

If a block ('#HW3') is a part of course_id, it is ignored.
If a block ('/block/HW3') is a part of course_id, it is ignored.

"""

Expand Down Expand Up @@ -295,6 +290,16 @@ def html_id(self):
"""
return self.course_id

def _set_value(self, parse, key, setter):
"""
Helper method that gets a value out of the dict returned by parse,
and then sets the corresponding bit of information in this locator
(via the supplied lambda 'setter'), unless the value is None.
"""
value = parse.get(key, None)
if value:
setter(value)


class BlockUsageLocator(CourseLocator):
"""
Expand Down Expand Up @@ -390,19 +395,15 @@ def init_block_ref_from_url(self, url):
url = url.url()
parse = parse_url(url)
assert parse, 'Could not parse "%s" as a url' % url
block = parse.get('block', None)
if block:
self.set_usage_id(block)
self._set_value(parse, 'block', lambda(new_block): self.set_usage_id(new_block))

def init_block_ref_from_course_id(self, course_id):
if isinstance(course_id, CourseLocator):
course_id = course_id.course_id
assert course_id, "%s does not have a valid course_id"
parse = parse_course_id(course_id)
assert parse, 'Could not parse "%s" as a course_id' % course_id
block = parse.get('block', None)
if block:
self.set_usage_id(block)
self._set_value(parse, 'block', lambda(new_block): self.set_usage_id(new_block))

def __unicode__(self):
"""
Expand All @@ -411,14 +412,14 @@ def __unicode__(self):
rep = CourseLocator.__unicode__(self)
if self.usage_id is None:
# usage_id has not been initialized
return rep + '#NONE'
return rep + BLOCK_PREFIX + 'NONE'
else:
return rep + '#' + self.usage_id
return rep + BLOCK_PREFIX + self.usage_id


class DescriptionLocator(Locator):
"""
Container for how to locate a description
Container for how to locate a description (the course-independent content).
"""

def __init__(self, definition_id):
Expand All @@ -427,22 +428,22 @@ def __init__(self, definition_id):
def __unicode__(self):
'''
Return a string representing this location.
unicode(self) returns something like this: "@519665f6223ebd6980884f2b"
unicode(self) returns something like this: "version/519665f6223ebd6980884f2b"
'''
return '@' + str(self.definition_guid)
return URL_VERSION_PREFIX + str(self.definition_id)

def url(self):
"""
Return a string containing the URL for this location.
url(self) returns something like this: 'edx://@519665f6223ebd6980884f2b'
url(self) returns something like this: 'edx://version/519665f6223ebd6980884f2b'
"""
return 'edx://' + unicode(self)

def version(self):
"""
Returns the ObjectId referencing this specific location.
"""
return self.definition_guid
return self.definition_id


class VersionTree(object):
Expand Down
47 changes: 32 additions & 15 deletions common/lib/xmodule/xmodule/modulestore/parsers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import re

# Prefix for the branch portion of a locator URL
BRANCH_PREFIX = "/branch/"
# Prefix for the block portion of a locator URL
BLOCK_PREFIX = "/block/"
# Prefix for the version portion of a locator URL, when it is preceded by a course ID
VERSION_PREFIX = "/version/"
# Prefix for version when it begins the URL (no course ID).
URL_VERSION_PREFIX = 'version/'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not consolidate these into '/?version/'? Since the regex will consume all alphanumerics (or alphanumerics plus periods) prior to the /version, there's no risk that foo.bar.version/branch will be grabbed as matching the version regex.


URL_RE = re.compile(r'^edx://(.+)$', re.IGNORECASE)


Expand All @@ -9,26 +18,27 @@ def parse_url(string):
followed by either a version_guid or a course_id.

Examples:
'edx://@0123FFFF'
'edx://version/0123FFFF'
'edx://edu.mit.eecs.6002x'
'edx://edu.mit.eecs.6002x;published'
'edx://edu.mit.eecs.6002x;published#HW3'
'edx://edu.mit.eecs.6002x/branch/published'
'edx://edu.mit.eecs.6002x/branch/published/version/519665f6223ebd6980884f2b/block/HW3'
'edx://edu.mit.eecs.6002x/branch/published/block/HW3'

This returns None if string cannot be parsed.

If it can be parsed as a version_guid, returns a dict
If it can be parsed as a version_guid with no preceding course_id, returns a dict
with key 'version_guid' and the value,

If it can be parsed as a course_id, returns a dict
with keys 'id' and 'branch' (value of 'branch' may be None),
with key 'id' and optional keys 'branch' and 'version_guid'.

"""
match = URL_RE.match(string)
if not match:
return None
path = match.group(1)
if path[0] == '@':
return parse_guid(path[1:])
if path.startswith(URL_VERSION_PREFIX):
return parse_guid(path[len(URL_VERSION_PREFIX):])
return parse_course_id(path)


Expand All @@ -52,7 +62,7 @@ def parse_block_ref(string):
return None


GUID_RE = re.compile(r'^(?P<version_guid>[A-F0-9]+)(#(?P<block>\w+))?$', re.IGNORECASE)
GUID_RE = re.compile(r'^(?P<version_guid>[A-F0-9]+)(' + BLOCK_PREFIX + '(?P<block>\w+))?$', re.IGNORECASE)


def parse_guid(string):
Expand All @@ -69,27 +79,34 @@ def parse_guid(string):
return None


COURSE_ID_RE = re.compile(r'^(?P<id>(\w+)(\.\w+\w*)*)(;(?P<branch>\w+))?(#(?P<block>\w+))?$', re.IGNORECASE)
COURSE_ID_RE = re.compile(
r'^(?P<id>(\w+)(\.\w+\w*)*)(' +
BRANCH_PREFIX + '(?P<branch>\w+))?(' +
VERSION_PREFIX + '(?P<version_guid>[A-F0-9]+))?(' +
BLOCK_PREFIX + '(?P<block>\w+))?$', re.IGNORECASE
)


def parse_course_id(string):
r"""

A course_id has a main id component.
There may also be an optional branch (;published or ;draft).
There may also be an optional block (#HW3 or #Quiz2).
There may also be an optional branch (/branch/published or /branch/draft).
There may also be an optional version (/version/519665f6223ebd6980884f2b).
There may also be an optional block (/block/HW3 or /block/Quiz2).

Examples of valid course_ids:

'edu.mit.eecs.6002x'
'edu.mit.eecs.6002x;published'
'edu.mit.eecs.6002x#HW3'
'edu.mit.eecs.6002x;published#HW3'
'edu.mit.eecs.6002x/branch/published'
'edu.mit.eecs.6002x/block/HW3'
'edu.mit.eecs.6002x/branch/published/block/HW3'
'edu.mit.eecs.6002x/branch/published/version/519665f6223ebd6980884f2b/block/HW3'


Syntax:

course_id = main_id [; branch] [# block]
course_id = main_id [/branch/ branch] [/version/ version ] [/block/ block]

main_id = name [. name]*

Expand Down
Loading