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
132 changes: 84 additions & 48 deletions common/lib/xmodule/xmodule/modulestore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

from collections import namedtuple

from abc import ABCMeta, abstractmethod

from .exceptions import InvalidLocationError, InsufficientSpecificationError
from xmodule.errortracker import make_error_tracker

log = logging.getLogger('mitx.' + 'modulestore')

SPLIT_MONGO_MODULESTORE_TYPE = 'split'
MONGO_MODULESTORE_TYPE = 'mongo'
XML_MODULESTORE_TYPE = 'xml'

Expand Down Expand Up @@ -254,17 +257,22 @@ def replace(self, **kwargs):
return self._replace(**kwargs)


class ModuleStore(object):
class ModuleStoreRead(object):
"""
An abstract interface for a database backend that stores XModuleDescriptor
instances
instances and extends read-only functionality
"""

__metaclass__ = ABCMeta

@abstractmethod
def has_item(self, course_id, location):
"""
Returns True if location exists in this ModuleStore.
"""
raise NotImplementedError
pass

@abstractmethod
def get_item(self, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at location.
Expand All @@ -282,15 +290,17 @@ def get_item(self, location, depth=0):
in the request. The depth is counted in the number of calls to
get_children() to cache. None indicates to cache all descendents
"""
raise NotImplementedError
pass

@abstractmethod
def get_instance(self, course_id, location, depth=0):
"""
Get an instance of this location, with policy for course_id applied.
TODO (vshnayder): this may want to live outside the modulestore eventually
"""
raise NotImplementedError
pass

@abstractmethod
def get_item_errors(self, location):
"""
Return a list of (msg, exception-or-None) errors that the modulestore
Expand All @@ -301,8 +311,9 @@ def get_item_errors(self, location):
Raises the same exceptions as get_item if the location isn't found or
isn't fully specified.
"""
raise NotImplementedError
pass

@abstractmethod
def get_items(self, location, course_id=None, depth=0):
"""
Returns a list of XModuleDescriptor instances for the items
Expand All @@ -316,8 +327,58 @@ def get_items(self, location, course_id=None, depth=0):
in the request. The depth is counted in the number of calls to
get_children() to cache. None indicates to cache all descendents
"""
raise NotImplementedError
pass

@abstractmethod
def get_courses(self):
'''
Returns a list containing the top level XModuleDescriptors of the courses
in this modulestore.
'''
pass

@abstractmethod
def get_course(self, course_id):
'''
Look for a specific course id. Returns the course descriptor, or None if not found.
'''
pass

@abstractmethod
def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location in this
course. Needed for path_to_location().

returns an iterable of things that can be passed to Location.
'''
pass

@abstractmethod
def get_errored_courses(self):
"""
Return a dictionary of course_dir -> [(msg, exception_str)], for each
course_dir where course loading failed.
"""
pass

@abstractmethod
def get_modulestore_type(self, course_id):
"""
Returns a type which identifies which modulestore is servicing the given
course_id. The return can be either "xml" (for XML based courses) or "mongo" for MongoDB backed courses
"""
pass


class ModuleStoreWrite(ModuleStoreRead):

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 does Write inherit from Read?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The thought was that we wanted Read permissions to be inherent in modules that can Write because we couldn't think of any places we would want write-only access.

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.

Ok

"""
An abstract interface for a database backend that stores XModuleDescriptor
instances and extends both read and write functionality
"""

__metaclass__ = ABCMeta

@abstractmethod
def update_item(self, location, data, allow_not_found=False):
"""
Set the data in the item specified by the location to
Expand All @@ -326,8 +387,9 @@ def update_item(self, location, data, allow_not_found=False):
location: Something that can be passed to Location
data: A nested dictionary of problem data
"""
raise NotImplementedError
pass

@abstractmethod
def update_children(self, location, children):
"""
Set the children for the item specified by the location to
Expand All @@ -336,8 +398,9 @@ def update_children(self, location, children):
location: Something that can be passed to Location
children: A list of child item identifiers
"""
raise NotImplementedError
pass

@abstractmethod
def update_metadata(self, location, metadata):
"""
Set the metadata for the item specified by the location to
Expand All @@ -346,58 +409,24 @@ def update_metadata(self, location, metadata):
location: Something that can be passed to Location
metadata: A nested dictionary of module metadata
"""
raise NotImplementedError
pass

@abstractmethod
def delete_item(self, location):
"""
Delete an item from this modulestore

location: Something that can be passed to Location
"""
raise NotImplementedError
pass

def get_courses(self):
'''
Returns a list containing the top level XModuleDescriptors of the courses
in this modulestore.
'''
course_filter = Location("i4x", category="course")
return self.get_items(course_filter)

def get_course(self, course_id):
'''
Look for a specific course id. Returns the course descriptor, or None if not found.
'''
raise NotImplementedError

def get_parent_locations(self, location, course_id):
'''Find all locations that are the parents of this location in this
course. Needed for path_to_location().

returns an iterable of things that can be passed to Location.
'''
raise NotImplementedError

def get_errored_courses(self):
"""
Return a dictionary of course_dir -> [(msg, exception_str)], for each
course_dir where course loading failed.
"""
raise NotImplementedError

def get_modulestore_type(self, course_id):
"""
Returns a type which identifies which modulestore is servicing the given
course_id. The return can be either "xml" (for XML based courses) or "mongo" for MongoDB backed courses
"""
raise NotImplementedError


class ModuleStoreBase(ModuleStore):
class ModuleStoreReadBase(ModuleStoreRead):
'''
Implement interface functionality that can be shared.
'''
# pylint: disable=W0613

def __init__(
self,
doc_store_config=None, # ignore if passed up
Expand Down Expand Up @@ -456,3 +485,10 @@ def get_course(self, course_id):
if c.id == course_id:
return c
return None


class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
'''
Implement interface functionality that can be shared.
'''
pass
11 changes: 7 additions & 4 deletions common/lib/xmodule/xmodule/modulestore/mixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
IMPORTANT: This modulestore only supports READONLY applications, e.g. LMS
"""

from . import ModuleStoreBase
from . import ModuleStoreWriteBase
from xmodule.modulestore.django import create_modulestore_instance
import logging

log = logging.getLogger(__name__)


class MixedModuleStore(ModuleStoreBase):
class MixedModuleStore(ModuleStoreWriteBase):
"""
ModuleStore that can be backed by either XML or Mongo
"""
Expand Down Expand Up @@ -138,8 +138,11 @@ def get_parent_locations(self, location, course_id):

def get_modulestore_type(self, course_id):
"""
Returns a type which identifies which modulestore is servicing the given
course_id. The return can be either "xml" (for XML based courses) or "mongo" for MongoDB backed courses
Returns a type which identifies which modulestore is servicing the given course_id.
The return can be one of:
"xml" (for XML based courses),
"mongo" for old-style MongoDB backed courses,
"split" for new-style split MongoDB backed courses.
"""
return self._get_modulestore_for_courseid(course_id).get_modulestore_type(course_id)

Expand Down
11 changes: 7 additions & 4 deletions common/lib/xmodule/xmodule/modulestore/mongo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from xblock.exceptions import InvalidScopeError
from xblock.fields import Scope, ScopeIds

from xmodule.modulestore import ModuleStoreBase, Location, MONGO_MODULESTORE_TYPE
from xmodule.modulestore import ModuleStoreWriteBase, Location, MONGO_MODULESTORE_TYPE
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
import re
Expand Down Expand Up @@ -251,7 +251,7 @@ def metadata_cache_key(location):
return u"{0.org}/{0.course}".format(location)


class MongoModuleStore(ModuleStoreBase):
class MongoModuleStore(ModuleStoreWriteBase):
"""
A Mongodb backed ModuleStore
"""
Expand Down Expand Up @@ -850,8 +850,11 @@ def get_parent_locations(self, location, course_id):

def get_modulestore_type(self, course_id):
"""
Returns a type which identifies which modulestore is servicing the given
course_id. The return can be either "xml" (for XML based courses) or "mongo" for MongoDB backed courses
Returns an enumeration-like type reflecting the type of this modulestore
The return can be one of:
"xml" (for XML based courses),
"mongo" for old-style MongoDB backed courses,
"split" for new-style split MongoDB backed courses.
"""
return MONGO_MODULESTORE_TYPE

Expand Down
14 changes: 12 additions & 2 deletions common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.locator import BlockUsageLocator, DefinitionLocator, CourseLocator, VersionTree, LocalId
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError
from xmodule.modulestore import inheritance, ModuleStoreBase, Location
from xmodule.modulestore import inheritance, ModuleStoreWriteBase, Location, SPLIT_MONGO_MODULESTORE_TYPE

from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader
Expand Down Expand Up @@ -44,7 +44,7 @@
#==============================================================================


class SplitMongoModuleStore(ModuleStoreBase):
class SplitMongoModuleStore(ModuleStoreWriteBase):
"""
A Mongodb backed ModuleStore supporting versions, inheritance,
and sharing.
Expand Down Expand Up @@ -1455,3 +1455,13 @@ def _filter_special_fields(self, fields):
if 'category' in fields:
del fields['category']
return fields

def get_modulestore_type(self, course_id):
"""
Returns an enumeration-like type reflecting the type of this modulestore
The return can be one of:
"xml" (for XML based courses),
"mongo" for old-style MongoDB backed courses,
"split" for new-style split MongoDB backed courses.
"""
return SPLIT_MONGO_MODULESTORE_TYPE
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Simple test to ensure that modulestore base classes remain abstract
"""
from unittest import TestCase

from xmodule.modulestore import ModuleStoreRead, ModuleStoreWrite


class AbstractionTest(TestCase):
"""
Tests that the ModuleStore objects are properly abstracted
"""

def test_cant_instantiate_abstract_class(self):
self.assertRaises(TypeError, ModuleStoreRead) # Cannot be instantiated due to explicit abstraction
self.assertRaises(TypeError, ModuleStoreWrite)
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.search import path_to_location


def check_path_to_location(modulestore):
'''Make sure that path_to_location works: should be passed a modulestore
with the toy and simple courses loaded.'''
"""
Make sure that path_to_location works: should be passed a modulestore
with the toy and simple courses loaded.
"""
should_work = (
("i4x://edX/toy/video/Welcome",
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
Expand Down
4 changes: 2 additions & 2 deletions common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from xblock.exceptions import InvalidScopeError

from xmodule.tests import DATA_DIR
from xmodule.modulestore import Location
from xmodule.modulestore import Location, MONGO_MODULESTORE_TYPE
from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore
from xmodule.modulestore.draft import DraftModuleStore
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
Expand Down Expand Up @@ -122,7 +122,7 @@ def test_mongo_modulestore_type(self):
{'host': HOST, 'db': DB, 'collection': COLLECTION},
FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS
)
assert_equals(store.get_modulestore_type('foo/bar/baz'), 'mongo')
assert_equals(store.get_modulestore_type('foo/bar/baz'), MONGO_MODULESTORE_TYPE)

def test_get_courses(self):
'''Make sure the course objects loaded properly'''
Expand Down
Loading