Commit 3cf29af8 by Calen Pennington

Make location into a named tuple, and use it more as a first class entity,…

Make location into a named tuple, and use it more as a first class entity, rather than URL for identifying content
parent 7ed9b4aa
...@@ -25,8 +25,8 @@ class Command(BaseCommand): ...@@ -25,8 +25,8 @@ class Command(BaseCommand):
module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor') module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor')
for module in module_store.modules.itervalues(): for module in module_store.modules.itervalues():
keystore().create_item(module.url) keystore().create_item(module.location)
if 'data' in module.definition: if 'data' in module.definition:
keystore().update_item(module.url, module.definition['data']) keystore().update_item(module.location, module.definition['data'])
if 'children' in module.definition: if 'children' in module.definition:
keystore().update_children(module.url, module.definition['children']) keystore().update_children(module.location, module.definition['children'])
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
% for week in weeks: % for week in weeks:
<li> <li>
<header> <header>
<h1><a href="#" class="module-edit" id="${week.url}">${week.name}</a></h1> <h1><a href="#" class="module-edit" id="${week.location.url()}">${week.name}</a></h1>
<ul> <ul>
% if week.goals: % if week.goals:
% for goal in week.goals: % for goal in week.goals:
...@@ -52,8 +52,8 @@ ...@@ -52,8 +52,8 @@
<ul> <ul>
% for module in week.get_children(): % for module in week.get_children():
<li class="${module.type}"> <li class="${module.category}">
<a href="#" class="module-edit" id="${module.url}">${module.name}</a> <a href="#" class="module-edit" id="${module.location.url()}">${module.name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
% endfor % endfor
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
<ol> <ol>
% for child in module.get_children(): % for child in module.get_children():
<li> <li>
<a href="#" class="module-edit" id="${child.url}">${child.name}</a> <a href="#" class="module-edit" id="${child.location.url()}">${child.name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
%endfor %endfor
......
...@@ -4,6 +4,7 @@ that are stored in a database an accessible using their Location as an identifie ...@@ -4,6 +4,7 @@ that are stored in a database an accessible using their Location as an identifie
""" """
import re import re
from collections import namedtuple
from .exceptions import InvalidLocationError from .exceptions import InvalidLocationError
URL_RE = re.compile(""" URL_RE = re.compile("""
...@@ -17,8 +18,8 @@ URL_RE = re.compile(""" ...@@ -17,8 +18,8 @@ URL_RE = re.compile("""
INVALID_CHARS = re.compile(r"[^\w.-]") INVALID_CHARS = re.compile(r"[^\w.-]")
_LocationBase = namedtuple('LocationBase', 'tag org course category name revision')
class Location(object): class Location(_LocationBase):
''' '''
Encodes a location. Encodes a location.
...@@ -28,6 +29,7 @@ class Location(object): ...@@ -28,6 +29,7 @@ class Location(object):
However, they can also be represented a dictionaries (specifying each component), However, they can also be represented a dictionaries (specifying each component),
tuples or list (specified in order), or as strings of the url tuples or list (specified in order), or as strings of the url
''' '''
__slots__ = ()
@classmethod @classmethod
def clean(cls, value): def clean(cls, value):
...@@ -36,7 +38,7 @@ class Location(object): ...@@ -36,7 +38,7 @@ class Location(object):
""" """
return re.sub('_+', '_', INVALID_CHARS.sub('_', value)) return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
def __init__(self, location): def __new__(_cls, loc_or_tag, org=None, course=None, category=None, name=None, revision=None):
""" """
Create a new location that is a clone of the specifed one. Create a new location that is a clone of the specifed one.
...@@ -60,47 +62,50 @@ class Location(object): ...@@ -60,47 +62,50 @@ class Location(object):
Components may be set to None, which may be interpreted by some contexts to mean Components may be set to None, which may be interpreted by some contexts to mean
wildcard selection wildcard selection
""" """
self.update(location)
def update(self, location): if org is None and course is None and category is None and name is None and revision is None:
""" location = loc_or_tag
Update this instance with data from another Location object. else:
location = (loc_or_tag, org, course, category, name, revision)
location: can take the same forms as specified by `__init__` def check_dict(dict_):
""" check_list(dict_.values())
self.tag = self.org = self.course = self.category = self.name = self.revision = None
def check_list(list_):
for val in list_:
if val is not None and INVALID_CHARS.search(val) is not None:
raise InvalidLocationError(location)
if isinstance(location, basestring): if isinstance(location, basestring):
match = URL_RE.match(location) match = URL_RE.match(location)
if match is None: if match is None:
raise InvalidLocationError(location) raise InvalidLocationError(location)
else: else:
self.update(match.groupdict()) groups = match.groupdict()
elif isinstance(location, list): check_dict(groups)
return _LocationBase.__new__(_cls, **groups)
elif isinstance(location, (list, tuple)):
if len(location) not in (5, 6): if len(location) not in (5, 6):
raise InvalidLocationError(location) raise InvalidLocationError(location)
(self.tag, self.org, self.course, self.category, self.name) = location[0:5] if len(location) == 5:
self.revision = location[5] if len(location) == 6 else None args = tuple(location) + (None, )
else:
args = tuple(location)
check_list(args)
return _LocationBase.__new__(_cls, *args)
elif isinstance(location, dict): elif isinstance(location, dict):
try: kwargs = dict(location)
self.tag = location['tag'] kwargs.setdefault('revision', None)
self.org = location['org']
self.course = location['course'] check_dict(kwargs)
self.category = location['category'] return _LocationBase.__new__(_cls, **kwargs)
self.name = location['name']
except KeyError:
raise InvalidLocationError(location)
self.revision = location.get('revision')
elif isinstance(location, Location): elif isinstance(location, Location):
self.update(location.list()) return _LocationBase.__new__(_cls, location)
else: else:
raise InvalidLocationError(location) raise InvalidLocationError(location)
for val in self.list():
if val is not None and INVALID_CHARS.search(val) is not None:
raise InvalidLocationError(location)
def url(self): def url(self):
""" """
Return a string containing the URL for this location Return a string containing the URL for this location
...@@ -114,38 +119,19 @@ class Location(object): ...@@ -114,38 +119,19 @@ class Location(object):
""" """
Return a string with a version of the location that is safe for use in html id attributes Return a string with a version of the location that is safe for use in html id attributes
""" """
return "-".join(str(v) for v in self.list() if v is not None) return "-".join(str(v) for v in self if v is not None)
def list(self):
"""
Return a list representing this location
"""
return [self.tag, self.org, self.course, self.category, self.name, self.revision]
def dict(self): def dict(self):
""" return self.__dict__
Return a dictionary representing this location
""" def list(self):
return {'tag': self.tag, return list(self)
'org': self.org,
'course': self.course,
'category': self.category,
'name': self.name,
'revision': self.revision}
def __str__(self): def __str__(self):
return self.url() return self.url()
def __repr__(self): def __repr__(self):
return 'Location(%r)' % str(self) return "Location%r" % tuple(self)
def __hash__(self):
return self.url()
def __eq__(self, other):
return (isinstance(other, Location) and
str(self) == str(other))
class ModuleStore(object): class ModuleStore(object):
""" """
......
from nose.tools import assert_equals, assert_raises from nose.tools import assert_equals, assert_raises, assert_not_equals
from keystore import Location from keystore import Location
from keystore.exceptions import InvalidLocationError from keystore.exceptions import InvalidLocationError
...@@ -11,7 +11,6 @@ def check_string_roundtrip(url): ...@@ -11,7 +11,6 @@ def check_string_roundtrip(url):
def test_string_roundtrip(): def test_string_roundtrip():
check_string_roundtrip("tag://org/course/category/name") check_string_roundtrip("tag://org/course/category/name")
check_string_roundtrip("tag://org/course/category/name/revision") check_string_roundtrip("tag://org/course/category/name/revision")
check_string_roundtrip("tag://org/course/category/name with spaces/revision")
def test_dict(): def test_dict():
...@@ -50,3 +49,15 @@ def test_invalid_locations(): ...@@ -50,3 +49,15 @@ def test_invalid_locations():
assert_raises(InvalidLocationError, Location, ["foo", "bar"]) assert_raises(InvalidLocationError, Location, ["foo", "bar"])
assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"]) assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"])
assert_raises(InvalidLocationError, Location, None) assert_raises(InvalidLocationError, Location, None)
assert_raises(InvalidLocationError, Location, "tag://org/course/category/name with spaces/revision")
def test_equality():
assert_equals(
Location('tag', 'org', 'course', 'category', 'name'),
Location('tag', 'org', 'course', 'category', 'name')
)
assert_not_equals(
Location('tag', 'org', 'course', 'category', 'name1'),
Location('tag', 'org', 'course', 'category', 'name')
)
...@@ -47,7 +47,7 @@ class XMLModuleStore(ModuleStore): ...@@ -47,7 +47,7 @@ class XMLModuleStore(ModuleStore):
xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules)) xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules))
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class) module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class)
modulestore.modules[module.url] = module modulestore.modules[module.location] = module
return module return module
XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml) XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml)
...@@ -68,7 +68,7 @@ class XMLModuleStore(ModuleStore): ...@@ -68,7 +68,7 @@ class XMLModuleStore(ModuleStore):
""" """
location = Location(location) location = Location(location)
try: try:
return self.modules[location.url()] return self.modules[location]
except KeyError: except KeyError:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
......
...@@ -102,7 +102,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor): ...@@ -102,7 +102,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
) )
child_content_urls = [ child_content_urls = [
system.process_xml(etree.tostring(child)).url system.process_xml(etree.tostring(child)).location.url()
for child in group for child in group
] ]
......
...@@ -105,6 +105,6 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -105,6 +105,6 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
return {'children': [ return {'children': [
system.process_xml(etree.tostring(child_module)).url system.process_xml(etree.tostring(child_module)).location.url()
for child_module in xml_object for child_module in xml_object
]} ]}
...@@ -239,7 +239,7 @@ class XModuleDescriptor(Plugin): ...@@ -239,7 +239,7 @@ class XModuleDescriptor(Plugin):
self.definition = definition if definition is not None else {} self.definition = definition if definition is not None else {}
self.name = Location(kwargs.get('location')).name self.name = Location(kwargs.get('location')).name
self.category = Location(kwargs.get('location')).category self.category = Location(kwargs.get('location')).category
self.url = Location(kwargs.get('location')).url() self.location = Location(kwargs.get('location'))
self.metadata = kwargs.get('metadata', {}) self.metadata = kwargs.get('metadata', {})
self.shared_state_key = kwargs.get('shared_state_key') self.shared_state_key = kwargs.get('shared_state_key')
...@@ -364,7 +364,7 @@ class XModuleDescriptor(Plugin): ...@@ -364,7 +364,7 @@ class XModuleDescriptor(Plugin):
return partial( return partial(
self.module_class, self.module_class,
system, system,
self.url, self.location,
self.definition, self.definition,
metadata=self.metadata metadata=self.metadata
) )
......
...@@ -85,6 +85,7 @@ class StudentModuleCache(object): ...@@ -85,6 +85,7 @@ class StudentModuleCache(object):
student=user, student=user,
module_state_key__in=id_chunk) module_state_key__in=id_chunk)
) )
else: else:
self.cache = [] self.cache = []
...@@ -93,7 +94,7 @@ class StudentModuleCache(object): ...@@ -93,7 +94,7 @@ class StudentModuleCache(object):
Get a list of the state_keys needed for StudentModules Get a list of the state_keys needed for StudentModules
required for this chunk of module xml required for this chunk of module xml
''' '''
keys = [descriptor.url] keys = [descriptor.location.url()]
shared_state_key = getattr(descriptor, 'shared_state_key', None) shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None: if shared_state_key is not None:
......
...@@ -207,7 +207,7 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -207,7 +207,7 @@ def get_module(user, request, location, student_module_cache, position=None):
''' '''
descriptor = keystore().get_item(location) descriptor = keystore().get_item(location)
instance_module = student_module_cache.lookup(descriptor.category, descriptor.url) instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url())
shared_state_key = getattr(descriptor, 'shared_state_key', None) shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not None: if shared_state_key is not None:
shared_module = student_module_cache.lookup(descriptor.category, shared_state_key) shared_module = student_module_cache.lookup(descriptor.category, shared_state_key)
...@@ -218,7 +218,7 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -218,7 +218,7 @@ def get_module(user, request, location, student_module_cache, position=None):
shared_state = shared_module.state if shared_module is not None else None shared_state = shared_module.state if shared_module is not None else None
# Setup system context for module instance # Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.url + '/' ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
def _get_module(location): def _get_module(location):
(module, _, _, _) = get_module(user, request, location, student_module_cache, position) (module, _, _, _) = get_module(user, request, location, student_module_cache, position)
......
...@@ -209,7 +209,7 @@ def index(request, course=None, chapter=None, section=None, ...@@ -209,7 +209,7 @@ def index(request, course=None, chapter=None, section=None,
course_location = multicourse_settings.get_course_location(course) course_location = multicourse_settings.get_course_location(course)
section = get_section(course_location, chapter, section) section = get_section(course_location, chapter, section)
student_module_cache = StudentModuleCache(request.user, section) student_module_cache = StudentModuleCache(request.user, section)
module, _, _, _ = get_module(request.user, request, section.url, student_module_cache) module, _, _, _ = get_module(request.user, request, section.location, student_module_cache)
context['content'] = module.get_html() context['content'] = module.get_html()
result = render_to_response('courseware.html', context) result = render_to_response('courseware.html', context)
......
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