Commit 9efe5d92 by Nimisha Asthagiri

Merge pull request #4720 from edx/split/add-and-fixes

Split/add and fixes
parents 26b08a02 d2b59cb6
...@@ -138,7 +138,7 @@ def xml_only_video(step): ...@@ -138,7 +138,7 @@ def xml_only_video(step):
course = world.scenario_dict['COURSE'] course = world.scenario_dict['COURSE']
store = modulestore() store = modulestore()
parent_location = store.get_items(course.id, category='vertical')[0].location parent_location = store.get_items(course.id, qualifiers={'category': 'vertical'})[0].location
youtube_id = 'ABCDEFG' youtube_id = 'ABCDEFG'
world.scenario_dict['YOUTUBE_ID'] = youtube_id world.scenario_dict['YOUTUBE_ID'] = youtube_id
......
...@@ -58,7 +58,7 @@ class Command(BaseCommand): ...@@ -58,7 +58,7 @@ class Command(BaseCommand):
discussion_items = _get_discussion_items(course) discussion_items = _get_discussion_items(course)
# now query all discussion items via get_items() and compare with the tree-traversal # now query all discussion items via get_items() and compare with the tree-traversal
queried_discussion_items = store.get_items(course_key=course_key, category='discussion',) queried_discussion_items = store.get_items(course_key=course_key, qualifiers={'category': 'discussion'})
for item in queried_discussion_items: for item in queried_discussion_items:
if item.location not in discussion_items: if item.location not in discussion_items:
......
...@@ -413,7 +413,7 @@ class CourseGradingTest(CourseTestCase): ...@@ -413,7 +413,7 @@ class CourseGradingTest(CourseTestCase):
Populate the course, grab a section, get the url for the assignment type access Populate the course, grab a section, get the url for the assignment type access
""" """
self.populate_course() self.populate_course()
sections = modulestore().get_items(self.course.id, category="sequential") sections = modulestore().get_items(self.course.id, qualifiers={'category': "sequential"})
# see if test makes sense # see if test makes sense
self.assertGreater(len(sections), 0, "No sections found") self.assertGreater(len(sections), 0, "No sections found")
section = sections[0] # just take the first one section = sections[0] # just take the first one
......
...@@ -14,7 +14,8 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -14,7 +14,8 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.tests.factories import check_exact_number_of_calls, check_number_of_calls from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore.tests.factories import check_exact_number_of_calls, check_number_of_calls, CourseFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
......
...@@ -190,7 +190,7 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -190,7 +190,7 @@ class CourseTestCase(ModuleStoreTestCase):
""" """
items = self.store.get_items( items = self.store.get_items(
course_id, course_id,
category='vertical', qualifiers={'category': 'vertical'},
revision=ModuleStoreEnum.RevisionOption.published_only revision=ModuleStoreEnum.RevisionOption.published_only
) )
self.check_verticals(items) self.check_verticals(items)
......
...@@ -277,7 +277,7 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked): ...@@ -277,7 +277,7 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
""" """
Helper method for formatting the asset information to send to client. Helper method for formatting the asset information to send to client.
""" """
asset_url = location.to_deprecated_string() asset_url = _add_slash(location.to_deprecated_string())
external_url = settings.LMS_BASE + asset_url external_url = settings.LMS_BASE + asset_url
return { return {
'display_name': display_name, 'display_name': display_name,
...@@ -285,8 +285,14 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked): ...@@ -285,8 +285,14 @@ def _get_asset_json(display_name, date, location, thumbnail_location, locked):
'url': asset_url, 'url': asset_url,
'external_url': external_url, 'external_url': external_url,
'portable_url': StaticContent.get_static_path_from_location(location), 'portable_url': StaticContent.get_static_path_from_location(location),
'thumbnail': thumbnail_location.to_deprecated_string() if thumbnail_location is not None else None, 'thumbnail': _add_slash(unicode(thumbnail_location)) if thumbnail_location else None,
'locked': locked, 'locked': locked,
# Needed for Backbone delete/update. # Needed for Backbone delete/update.
'id': unicode(location) 'id': unicode(location)
} }
def _add_slash(url):
if not url.startswith('/'):
url = '/' + url # TODO - re-address this once LMS-11198 is tackled.
return url
...@@ -24,9 +24,9 @@ from xmodule.contentstore.content import StaticContent ...@@ -24,9 +24,9 @@ from xmodule.contentstore.content import StaticContent
from xmodule.tabs import PDFTextbookTabs from xmodule.tabs import PDFTextbookTabs
from xmodule.partitions.partitions import UserPartition, Group from xmodule.partitions.partitions import UserPartition, Group
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import Location, SlashSeparatedCourseKey from opaque_keys.edx.locations import Location
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils import ( from contentstore.utils import (
...@@ -441,7 +441,7 @@ def _create_or_rerun_course(request): ...@@ -441,7 +441,7 @@ def _create_or_rerun_course(request):
""" """
To be called by requests that create a new destination course (i.e., create_new_course and rerun_course) To be called by requests that create a new destination course (i.e., create_new_course and rerun_course)
Returns the destination course_key and overriding fields for the new course. Returns the destination course_key and overriding fields for the new course.
Raises InvalidLocationError and InvalidKeyError Raises DuplicateCourseError and InvalidKeyError
""" """
if not auth.has_access(request.user, CourseCreatorRole()): if not auth.has_access(request.user, CourseCreatorRole()):
raise PermissionDenied() raise PermissionDenied()
...@@ -460,15 +460,14 @@ def _create_or_rerun_course(request): ...@@ -460,15 +460,14 @@ def _create_or_rerun_course(request):
status=400 status=400
) )
course_key = SlashSeparatedCourseKey(org, number, run)
fields = {'display_name': display_name} if display_name is not None else {} fields = {'display_name': display_name} if display_name is not None else {}
if 'source_course_key' in request.json: if 'source_course_key' in request.json:
return _rerun_course(request, course_key, fields) return _rerun_course(request, org, number, run, fields)
else: else:
return _create_new_course(request, course_key, fields) return _create_new_course(request, org, number, run, fields)
except InvalidLocationError: except DuplicateCourseError:
return JsonResponse({ return JsonResponse({
'ErrMsg': _( 'ErrMsg': _(
'There is already a course defined with the same ' 'There is already a course defined with the same '
...@@ -488,27 +487,30 @@ def _create_or_rerun_course(request): ...@@ -488,27 +487,30 @@ def _create_or_rerun_course(request):
) )
def _create_new_course(request, course_key, fields): def _create_new_course(request, org, number, run, fields):
""" """
Create a new course. Create a new course.
Returns the URL for the course overview page. Returns the URL for the course overview page.
Raises DuplicateCourseError if the course already exists
""" """
# Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for # Set a unique wiki_slug for newly created courses. To maintain active wiki_slugs for
# existing xml courses this cannot be changed in CourseDescriptor. # existing xml courses this cannot be changed in CourseDescriptor.
# # TODO get rid of defining wiki slug in this org/course/run specific way and reconcile # # TODO get rid of defining wiki slug in this org/course/run specific way and reconcile
# w/ xmodule.course_module.CourseDescriptor.__init__ # w/ xmodule.course_module.CourseDescriptor.__init__
wiki_slug = u"{0}.{1}.{2}".format(course_key.org, course_key.course, course_key.run) wiki_slug = u"{0}.{1}.{2}".format(org, number, run)
definition_data = {'wiki_slug': wiki_slug} definition_data = {'wiki_slug': wiki_slug}
fields.update(definition_data) fields.update(definition_data)
# Creating the course raises InvalidLocationError if an existing course with this org/name is found store = modulestore()
new_course = modulestore().create_course( with store.default_store(settings.FEATURES.get('DEFAULT_STORE_FOR_NEW_COURSE', 'mongo')):
course_key.org, # Creating the course raises DuplicateCourseError if an existing course with this org/name is found
course_key.course, new_course = store.create_course(
course_key.run, org,
request.user.id, number,
fields=fields, run,
) request.user.id,
fields=fields,
)
# Make sure user has instructor and staff access to the new course # Make sure user has instructor and staff access to the new course
add_instructor(new_course.id, request.user, request.user) add_instructor(new_course.id, request.user, request.user)
...@@ -521,7 +523,7 @@ def _create_new_course(request, course_key, fields): ...@@ -521,7 +523,7 @@ def _create_new_course(request, course_key, fields):
}) })
def _rerun_course(request, destination_course_key, fields): def _rerun_course(request, org, number, run, fields):
""" """
Reruns an existing course. Reruns an existing course.
Returns the URL for the course listing page. Returns the URL for the course listing page.
...@@ -532,6 +534,15 @@ def _rerun_course(request, destination_course_key, fields): ...@@ -532,6 +534,15 @@ def _rerun_course(request, destination_course_key, fields):
if not has_course_access(request.user, source_course_key): if not has_course_access(request.user, source_course_key):
raise PermissionDenied() raise PermissionDenied()
# create destination course key
store = modulestore()
with store.default_store('split'):
destination_course_key = store.make_course_key(org, number, run)
# verify org course and run don't already exist
if store.has_course(destination_course_key, ignore_case=True):
raise DuplicateCourseError(source_course_key, destination_course_key)
# Make sure user has instructor and staff access to the destination course # Make sure user has instructor and staff access to the destination course
# so the user can see the updated status for that course # so the user can see the updated status for that course
add_instructor(destination_course_key, request.user, request.user) add_instructor(destination_course_key, request.user, request.user)
...@@ -540,10 +551,13 @@ def _rerun_course(request, destination_course_key, fields): ...@@ -540,10 +551,13 @@ def _rerun_course(request, destination_course_key, fields):
CourseRerunState.objects.initiated(source_course_key, destination_course_key, request.user) CourseRerunState.objects.initiated(source_course_key, destination_course_key, request.user)
# Rerun the course as a new celery task # Rerun the course as a new celery task
rerun_course.delay(source_course_key, destination_course_key, request.user.id, fields) rerun_course.delay(unicode(source_course_key), unicode(destination_course_key), request.user.id, fields)
# Return course listing page # Return course listing page
return JsonResponse({'url': reverse_url('course_handler')}) return JsonResponse({
'url': reverse_url('course_handler'),
'destination_course_key': unicode(destination_course_key)
})
# pylint: disable=unused-argument # pylint: disable=unused-argument
...@@ -1121,7 +1135,7 @@ class GroupConfiguration(object): ...@@ -1121,7 +1135,7 @@ class GroupConfiguration(object):
} }
""" """
usage_info = {} usage_info = {}
descriptors = store.get_items(course.id, category='split_test') descriptors = store.get_items(course.id, qualifiers={'category': 'split_test'})
for split_test in descriptors: for split_test in descriptors:
if split_test.user_partition_id not in usage_info: if split_test.user_partition_id not in usage_info:
usage_info[split_test.user_partition_id] = [] usage_info[split_test.user_partition_id] = []
......
...@@ -424,6 +424,11 @@ def _create_item(request): ...@@ -424,6 +424,11 @@ def _create_item(request):
if display_name is not None: if display_name is not None:
metadata['display_name'] = display_name metadata['display_name'] = display_name
# TODO need to fix components that are sending definition_data as strings, instead of as dicts
# For now, migrate them into dicts here.
if isinstance(data, basestring):
data = {'data': data}
created_block = store.create_child( created_block = store.create_child(
request.user.id, request.user.id,
usage_key, usage_key,
......
...@@ -5,17 +5,27 @@ This file contains celery tasks for contentstore views ...@@ -5,17 +5,27 @@ This file contains celery tasks for contentstore views
from celery.task import task from celery.task import task
from django.contrib.auth.models import User from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
from course_action_state.models import CourseRerunState from course_action_state.models import CourseRerunState
from contentstore.utils import initialize_permissions from contentstore.utils import initialize_permissions
from opaque_keys.edx.keys import CourseKey
@task() @task()
def rerun_course(source_course_key, destination_course_key, user_id, fields=None): def rerun_course(source_course_key_string, destination_course_key_string, user_id, fields=None):
""" """
Reruns a course in a new celery task. Reruns a course in a new celery task.
""" """
try: try:
modulestore().clone_course(source_course_key, destination_course_key, user_id, fields=fields) # deserialize the keys
source_course_key = CourseKey.from_string(source_course_key_string)
destination_course_key = CourseKey.from_string(destination_course_key_string)
# use the split modulestore as the store for the rerun course,
# as the Mongo modulestore doesn't support multiple runs of the same course.
store = modulestore()
with store.default_store('split'):
store.clone_course(source_course_key, destination_course_key, user_id, fields=fields)
# set initial permissions for the user to access the course. # set initial permissions for the user to access the course.
initialize_permissions(destination_course_key, User.objects.get(id=user_id)) initialize_permissions(destination_course_key, User.objects.get(id=user_id))
...@@ -23,10 +33,24 @@ def rerun_course(source_course_key, destination_course_key, user_id, fields=None ...@@ -23,10 +33,24 @@ def rerun_course(source_course_key, destination_course_key, user_id, fields=None
# update state: Succeeded # update state: Succeeded
CourseRerunState.objects.succeeded(course_key=destination_course_key) CourseRerunState.objects.succeeded(course_key=destination_course_key)
return "succeeded"
except DuplicateCourseError as exc:
# do NOT delete the original course, only update the status
CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc)
return "duplicate course"
# catch all exceptions so we can update the state and properly cleanup the course. # catch all exceptions so we can update the state and properly cleanup the course.
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
# update state: Failed # update state: Failed
CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc) CourseRerunState.objects.failed(course_key=destination_course_key, exception=exc)
# cleanup any remnants of the course try:
modulestore().delete_course(destination_course_key, user_id) # cleanup any remnants of the course
modulestore().delete_course(destination_course_key, user_id)
except ItemNotFoundError:
# it's possible there was an error even before the course module was created
pass
return "exception: " + unicode(exc)
...@@ -51,7 +51,6 @@ ...@@ -51,7 +51,6 @@
"ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore",
"OPTIONS": { "OPTIONS": {
"mappings": {}, "mappings": {},
"reference_type": "Location",
"stores": [ "stores": [
{ {
"NAME": "draft", "NAME": "draft",
......
...@@ -106,6 +106,9 @@ FEATURES = { ...@@ -106,6 +106,9 @@ FEATURES = {
# Toggles Group Configuration editing functionality # Toggles Group Configuration editing functionality
'ENABLE_GROUP_CONFIGURATIONS': os.environ.get('FEATURE_GROUP_CONFIGURATIONS'), 'ENABLE_GROUP_CONFIGURATIONS': os.environ.get('FEATURE_GROUP_CONFIGURATIONS'),
# Modulestore to use for new courses
'DEFAULT_STORE_FOR_NEW_COURSE': 'mongo',
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
......
...@@ -10,6 +10,7 @@ Core methods ...@@ -10,6 +10,7 @@ Core methods
from django.core.cache import cache from django.core.cache import cache
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
from opaque_keys import InvalidKeyError
from . import app_settings from . import app_settings
...@@ -118,9 +119,19 @@ def get_cached_content(location): ...@@ -118,9 +119,19 @@ def get_cached_content(location):
def del_cached_content(location): def del_cached_content(location):
# delete content for the given location, as well as for content with run=None. """
# it's possible that the content could have been cached without knowing the delete content for the given location, as well as for content with run=None.
# course_key - and so without having the run. it's possible that the content could have been cached without knowing the
cache.delete_many( course_key - and so without having the run.
[unicode(loc).encode("utf-8") for loc in [location, location.replace(run=None)]] """
) def location_str(loc):
return unicode(loc).encode("utf-8")
locations = [location_str(location)]
try:
locations.append(location_str(location.replace(run=None)))
except InvalidKeyError:
# although deprecated keys allowed run=None, new keys don't if there is no version.
pass
cache.delete_many(locations)
...@@ -6,6 +6,7 @@ from xmodule.contentstore.django import contentstore ...@@ -6,6 +6,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
from xmodule.modulestore import InvalidLocationError from xmodule.modulestore import InvalidLocationError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.locator import AssetLocator
from cache_toolbox.core import get_cached_content, set_cached_content from cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
...@@ -14,8 +15,11 @@ from xmodule.exceptions import NotFoundError ...@@ -14,8 +15,11 @@ from xmodule.exceptions import NotFoundError
class StaticContentServer(object): class StaticContentServer(object):
def process_request(self, request): def process_request(self, request):
# look to see if the request is prefixed with 'c4x' tag # look to see if the request is prefixed with an asset prefix tag
if request.path.startswith('/' + XASSET_LOCATION_TAG + '/'): if (
request.path.startswith('/' + XASSET_LOCATION_TAG + '/') or
request.path.startswith('/' + AssetLocator.CANONICAL_NAMESPACE)
):
try: try:
loc = StaticContent.get_location_from_path(request.path) loc = StaticContent.get_location_from_path(request.path)
except (InvalidLocationError, InvalidKeyError): except (InvalidLocationError, InvalidKeyError):
......
...@@ -5,7 +5,7 @@ Adds user's tags to tracking event context. ...@@ -5,7 +5,7 @@ Adds user's tags to tracking event context.
from eventtracking import tracker from eventtracking import tracker
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.keys import CourseKey
from track.contexts import COURSE_REGEX from track.contexts import COURSE_REGEX
from user_api.models import UserCourseTag from user_api.models import UserCourseTag
...@@ -24,7 +24,7 @@ class UserTagsEventContextMiddleware(object): ...@@ -24,7 +24,7 @@ class UserTagsEventContextMiddleware(object):
if match: if match:
course_id = match.group('course_id') course_id = match.group('course_id')
try: try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = CourseKey.from_string(course_id)
except InvalidKeyError: except InvalidKeyError:
course_id = None course_id = None
course_key = None course_key = None
......
...@@ -2,6 +2,7 @@ from django.db import models ...@@ -2,6 +2,7 @@ from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import Locator
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^xmodule_django\.models\.CourseKeyField"]) add_introspection_rules([], ["^xmodule_django\.models\.CourseKeyField"])
...@@ -44,6 +45,28 @@ class NoneToEmptyQuerySet(models.query.QuerySet): ...@@ -44,6 +45,28 @@ class NoneToEmptyQuerySet(models.query.QuerySet):
return super(NoneToEmptyQuerySet, self)._filter_or_exclude(*args, **kwargs) return super(NoneToEmptyQuerySet, self)._filter_or_exclude(*args, **kwargs)
def _strip_object(key):
"""
Strips branch and version info if the given key supports those attributes.
"""
if hasattr(key, 'version_agnostic') and hasattr(key, 'for_branch'):
return key.for_branch(None).version_agnostic()
else:
return key
def _strip_value(value, lookup='exact'):
"""
Helper function to remove the branch and version information from the given value,
which could be a single object or a list.
"""
if lookup == 'in':
stripped_value = [_strip_object(el) for el in value]
else:
stripped_value = _strip_object(value)
return stripped_value
class CourseKeyField(models.CharField): class CourseKeyField(models.CharField):
description = "A SlashSeparatedCourseKey object, saved to the DB in the form of a string" description = "A SlashSeparatedCourseKey object, saved to the DB in the form of a string"
...@@ -69,14 +92,18 @@ class CourseKeyField(models.CharField): ...@@ -69,14 +92,18 @@ class CourseKeyField(models.CharField):
if lookup == 'isnull': if lookup == 'isnull':
raise TypeError('Use CourseKeyField.Empty rather than None to query for a missing CourseKeyField') raise TypeError('Use CourseKeyField.Empty rather than None to query for a missing CourseKeyField')
return super(CourseKeyField, self).get_prep_lookup(lookup, value) return super(CourseKeyField, self).get_prep_lookup(
lookup,
# strip key before comparing
_strip_value(value, lookup)
)
def get_prep_value(self, value): def get_prep_value(self, value):
if value is self.Empty or value is None: if value is self.Empty or value is None:
return '' # CharFields should use '' as their empty value, rather than None return '' # CharFields should use '' as their empty value, rather than None
assert isinstance(value, CourseKey) assert isinstance(value, CourseKey)
return value.to_deprecated_string() return unicode(_strip_value(value))
def validate(self, value, model_instance): def validate(self, value, model_instance):
"""Validate Empty values, otherwise defer to the parent""" """Validate Empty values, otherwise defer to the parent"""
...@@ -119,14 +146,19 @@ class LocationKeyField(models.CharField): ...@@ -119,14 +146,19 @@ class LocationKeyField(models.CharField):
if lookup == 'isnull': if lookup == 'isnull':
raise TypeError('Use LocationKeyField.Empty rather than None to query for a missing LocationKeyField') raise TypeError('Use LocationKeyField.Empty rather than None to query for a missing LocationKeyField')
return super(LocationKeyField, self).get_prep_lookup(lookup, value) # remove version and branch info before comparing keys
return super(LocationKeyField, self).get_prep_lookup(
lookup,
# strip key before comparing
_strip_value(value, lookup)
)
def get_prep_value(self, value): def get_prep_value(self, value):
if value is self.Empty: if value is self.Empty:
return '' return ''
assert isinstance(value, UsageKey) assert isinstance(value, UsageKey)
return value.to_deprecated_string() return unicode(_strip_value(value))
def validate(self, value, model_instance): def validate(self, value, model_instance):
"""Validate Empty values, otherwise defer to the parent""" """Validate Empty values, otherwise defer to the parent"""
......
...@@ -10,8 +10,9 @@ import StringIO ...@@ -10,8 +10,9 @@ import StringIO
from urlparse import urlparse, urlunparse, parse_qsl from urlparse import urlparse, urlunparse, parse_qsl
from urllib import urlencode from urllib import urlencode
from opaque_keys.edx.locations import AssetLocation from opaque_keys.edx.locator import AssetLocator
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey, AssetKey
from opaque_keys import InvalidKeyError
from PIL import Image from PIL import Image
...@@ -52,12 +53,10 @@ class StaticContent(object): ...@@ -52,12 +53,10 @@ class StaticContent(object):
asset asset
""" """
path = path.replace('/', '_') path = path.replace('/', '_')
return AssetLocation( return course_key.make_asset_key(
course_key.org, course_key.course, course_key.run,
'asset' if not is_thumbnail else 'thumbnail', 'asset' if not is_thumbnail else 'thumbnail',
AssetLocation.clean_keeping_underscores(path), AssetLocator.clean_keeping_underscores(path)
revision ).for_branch(None)
)
def get_id(self): def get_id(self):
return self.location return self.location
...@@ -104,16 +103,22 @@ class StaticContent(object): ...@@ -104,16 +103,22 @@ class StaticContent(object):
return None return None
assert(isinstance(course_key, CourseKey)) assert(isinstance(course_key, CourseKey))
return course_key.make_asset_key('asset', '').to_deprecated_string() # create a dummy asset location and then strip off the last character: 'a',
# since the AssetLocator rejects the empty string as a legal value for the block_id.
return course_key.make_asset_key('asset', 'a').for_branch(None).to_deprecated_string()[:-1]
@staticmethod @staticmethod
def get_location_from_path(path): def get_location_from_path(path):
""" """
Generate an AssetKey for the given path (old c4x/org/course/asset/name syntax) Generate an AssetKey for the given path (old c4x/org/course/asset/name syntax)
""" """
# TODO OpaqueKeys after opaque keys deprecation is working try:
# return AssetLocation.from_string(path) return AssetKey.from_string(path)
return AssetLocation.from_deprecated_string(path) except InvalidKeyError:
# TODO - re-address this once LMS-11198 is tackled.
if path.startswith('/'):
# try stripping off the leading slash and try again
return AssetKey.from_string(path[1:])
@staticmethod @staticmethod
def convert_legacy_static_url_with_course_id(path, course_id): def convert_legacy_static_url_with_course_id(path, course_id):
......
...@@ -94,7 +94,10 @@ class MongoContentStore(ContentStore): ...@@ -94,7 +94,10 @@ class MongoContentStore(ContentStore):
fp = self.fs.get(content_id) fp = self.fs.get(content_id)
thumbnail_location = getattr(fp, 'thumbnail_location', None) thumbnail_location = getattr(fp, 'thumbnail_location', None)
if thumbnail_location: if thumbnail_location:
thumbnail_location = location.course_key.make_asset_key('thumbnail', thumbnail_location[4]) thumbnail_location = location.course_key.make_asset_key(
'thumbnail',
thumbnail_location[4]
)
return StaticContentStream( return StaticContentStream(
location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate, location, fp.displayname, fp.content_type, fp, last_modified_at=fp.uploadDate,
thumbnail_location=thumbnail_location, thumbnail_location=thumbnail_location,
...@@ -105,7 +108,10 @@ class MongoContentStore(ContentStore): ...@@ -105,7 +108,10 @@ class MongoContentStore(ContentStore):
with self.fs.get(content_id) as fp: with self.fs.get(content_id) as fp:
thumbnail_location = getattr(fp, 'thumbnail_location', None) thumbnail_location = getattr(fp, 'thumbnail_location', None)
if thumbnail_location: if thumbnail_location:
thumbnail_location = location.course_key.make_asset_key('thumbnail', thumbnail_location[4]) thumbnail_location = location.course_key.make_asset_key(
'thumbnail',
thumbnail_location[4]
)
return StaticContent( return StaticContent(
location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate, location, fp.displayname, fp.content_type, fp.read(), last_modified_at=fp.uploadDate,
thumbnail_location=thumbnail_location, thumbnail_location=thumbnail_location,
...@@ -304,7 +310,9 @@ class MongoContentStore(ContentStore): ...@@ -304,7 +310,9 @@ class MongoContentStore(ContentStore):
asset_id = asset_key asset_id = asset_key
else: # add the run, since it's the last field, we're golden else: # add the run, since it's the last field, we're golden
asset_key['run'] = dest_course_key.run asset_key['run'] = dest_course_key.run
asset_id = unicode(dest_course_key.make_asset_key(asset_key['category'], asset_key['name'])) asset_id = unicode(
dest_course_key.make_asset_key(asset_key['category'], asset_key['name']).for_branch(None)
)
self.fs.put( self.fs.put(
source_content.read(), source_content.read(),
...@@ -347,7 +355,7 @@ class MongoContentStore(ContentStore): ...@@ -347,7 +355,7 @@ class MongoContentStore(ContentStore):
# NOTE, there's no need to state that run doesn't exist in the negative case b/c access via # NOTE, there's no need to state that run doesn't exist in the negative case b/c access via
# SON requires equivalence (same keys and values in exact same order) # SON requires equivalence (same keys and values in exact same order)
dbkey['run'] = location.run dbkey['run'] = location.run
content_id = unicode(location) content_id = unicode(location.for_branch(None))
return content_id, dbkey return content_id, dbkey
def make_id_son(self, fs_entry): def make_id_son(self, fs_entry):
......
...@@ -116,7 +116,7 @@ class ModuleStoreRead(object): ...@@ -116,7 +116,7 @@ class ModuleStoreRead(object):
pass pass
@abstractmethod @abstractmethod
def get_item(self, usage_key, depth=0): def get_item(self, usage_key, depth=0, **kwargs):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
...@@ -150,7 +150,7 @@ class ModuleStoreRead(object): ...@@ -150,7 +150,7 @@ class ModuleStoreRead(object):
pass pass
@abstractmethod @abstractmethod
def get_items(self, location, course_id=None, depth=0, qualifiers=None): def get_items(self, location, course_id=None, depth=0, qualifiers=None, **kwargs):
""" """
Returns a list of XModuleDescriptor instances for the items Returns a list of XModuleDescriptor instances for the items
that match location. Any element of location that is None is treated that match location. Any element of location that is None is treated
...@@ -228,7 +228,17 @@ class ModuleStoreRead(object): ...@@ -228,7 +228,17 @@ class ModuleStoreRead(object):
return criteria == target return criteria == target
@abstractmethod @abstractmethod
def get_courses(self): def make_course_key(self, org, course, run):
"""
Return a valid :class:`~opaque_keys.edx.keys.CourseKey` for this modulestore
that matches the supplied `org`, `course`, and `run`.
This key may represent a course that doesn't exist in this modulestore.
"""
pass
@abstractmethod
def get_courses(self, **kwargs):
''' '''
Returns a list containing the top level XModuleDescriptors of the courses Returns a list containing the top level XModuleDescriptors of the courses
in this modulestore. in this modulestore.
...@@ -236,7 +246,7 @@ class ModuleStoreRead(object): ...@@ -236,7 +246,7 @@ class ModuleStoreRead(object):
pass pass
@abstractmethod @abstractmethod
def get_course(self, course_id, depth=0): def get_course(self, course_id, depth=0, **kwargs):
''' '''
Look for a specific course by its id (:class:`CourseKey`). Look for a specific course by its id (:class:`CourseKey`).
Returns the course descriptor, or None if not found. Returns the course descriptor, or None if not found.
...@@ -244,7 +254,7 @@ class ModuleStoreRead(object): ...@@ -244,7 +254,7 @@ class ModuleStoreRead(object):
pass pass
@abstractmethod @abstractmethod
def has_course(self, course_id, ignore_case=False): def has_course(self, course_id, ignore_case=False, **kwargs):
''' '''
Look for a specific course id. Returns whether it exists. Look for a specific course id. Returns whether it exists.
Args: Args:
...@@ -256,13 +266,14 @@ class ModuleStoreRead(object): ...@@ -256,13 +266,14 @@ class ModuleStoreRead(object):
@abstractmethod @abstractmethod
def get_parent_location(self, location, **kwargs): def get_parent_location(self, location, **kwargs):
'''Find the location that is the parent of this location in this '''
Find the location that is the parent of this location in this
course. Needed for path_to_location(). course. Needed for path_to_location().
''' '''
pass pass
@abstractmethod @abstractmethod
def get_orphans(self, course_key): def get_orphans(self, course_key, **kwargs):
""" """
Get all of the xblocks in the given course which have no parents and are not of types which are Get all of the xblocks in the given course which have no parents and are not of types which are
usually orphaned. NOTE: may include xblocks which still have references via xblocks which don't usually orphaned. NOTE: may include xblocks which still have references via xblocks which don't
...@@ -287,7 +298,7 @@ class ModuleStoreRead(object): ...@@ -287,7 +298,7 @@ class ModuleStoreRead(object):
pass pass
@abstractmethod @abstractmethod
def get_courses_for_wiki(self, wiki_slug): def get_courses_for_wiki(self, wiki_slug, **kwargs):
""" """
Return the list of courses which use this wiki_slug Return the list of courses which use this wiki_slug
:param wiki_slug: the course wiki root slug :param wiki_slug: the course wiki root slug
...@@ -325,7 +336,7 @@ class ModuleStoreWrite(ModuleStoreRead): ...@@ -325,7 +336,7 @@ class ModuleStoreWrite(ModuleStoreRead):
__metaclass__ = ABCMeta __metaclass__ = ABCMeta
@abstractmethod @abstractmethod
def update_item(self, xblock, user_id, allow_not_found=False, force=False): def update_item(self, xblock, user_id, allow_not_found=False, force=False, **kwargs):
""" """
Update the given xblock's persisted repr. Pass the user's unique id which the persistent store Update the given xblock's persisted repr. Pass the user's unique id which the persistent store
should save with the update if it has that ability. should save with the update if it has that ability.
...@@ -413,7 +424,7 @@ class ModuleStoreWrite(ModuleStoreRead): ...@@ -413,7 +424,7 @@ class ModuleStoreWrite(ModuleStoreRead):
pass pass
@abstractmethod @abstractmethod
def delete_course(self, course_key, user_id): def delete_course(self, course_key, user_id, **kwargs):
""" """
Deletes the course. It may be a soft or hard delete. It may or may not remove the xblock definitions Deletes the course. It may be a soft or hard delete. It may or may not remove the xblock definitions
depending on the persistence layer and how tightly bound the xblocks are to the course. depending on the persistence layer and how tightly bound the xblocks are to the course.
...@@ -480,19 +491,19 @@ class ModuleStoreReadBase(ModuleStoreRead): ...@@ -480,19 +491,19 @@ class ModuleStoreReadBase(ModuleStoreRead):
""" """
return {} return {}
def get_course(self, course_id, depth=0): def get_course(self, course_id, depth=0, **kwargs):
""" """
See ModuleStoreRead.get_course See ModuleStoreRead.get_course
Default impl--linear search through course list Default impl--linear search through course list
""" """
assert(isinstance(course_id, CourseKey)) assert(isinstance(course_id, CourseKey))
for course in self.get_courses(): for course in self.get_courses(**kwargs):
if course.id == course_id: if course.id == course_id:
return course return course
return None return None
def has_course(self, course_id, ignore_case=False): def has_course(self, course_id, ignore_case=False, **kwargs):
""" """
Returns the course_id of the course if it was found, else None Returns the course_id of the course if it was found, else None
Args: Args:
...@@ -577,7 +588,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -577,7 +588,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
result[field.scope][field_name] = value result[field.scope][field_name] = value
return result return result
def clone_course(self, source_course_id, dest_course_id, user_id, fields=None): def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs):
""" """
This base method just copies the assets. The lower level impls must do the actual cloning of This base method just copies the assets. The lower level impls must do the actual cloning of
content. content.
...@@ -587,7 +598,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -587,7 +598,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
self.contentstore.copy_all_course_assets(source_course_id, dest_course_id) self.contentstore.copy_all_course_assets(source_course_id, dest_course_id)
return dest_course_id return dest_course_id
def delete_course(self, course_key, user_id): def delete_course(self, course_key, user_id, **kwargs):
""" """
This base method just deletes the assets. The lower level impls must do the actual deleting of This base method just deletes the assets. The lower level impls must do the actual deleting of
content. content.
......
...@@ -3,6 +3,7 @@ This file contains helper functions for configuring module_store_setting setting ...@@ -3,6 +3,7 @@ This file contains helper functions for configuring module_store_setting setting
""" """
import warnings import warnings
import copy
def convert_module_store_setting_if_needed(module_store_setting): def convert_module_store_setting_if_needed(module_store_setting):
...@@ -42,7 +43,6 @@ def convert_module_store_setting_if_needed(module_store_setting): ...@@ -42,7 +43,6 @@ def convert_module_store_setting_if_needed(module_store_setting):
"ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore",
"OPTIONS": { "OPTIONS": {
"mappings": {}, "mappings": {},
"reference_type": "Location",
"stores": [] "stores": []
} }
} }
...@@ -66,6 +66,27 @@ def convert_module_store_setting_if_needed(module_store_setting): ...@@ -66,6 +66,27 @@ def convert_module_store_setting_if_needed(module_store_setting):
) )
assert isinstance(module_store_setting['default']['OPTIONS']['stores'], list) assert isinstance(module_store_setting['default']['OPTIONS']['stores'], list)
# If Split is not defined but the DraftMongoModuleStore is configured, add Split as a copy of Draft
mixed_stores = module_store_setting['default']['OPTIONS']['stores']
is_split_defined = any((store['ENGINE'].endswith('.DraftVersioningModuleStore')) for store in mixed_stores)
if not is_split_defined:
# find first setting of mongo store
mongo_store = next(
(store for store in mixed_stores if (
store['ENGINE'].endswith('.DraftMongoModuleStore') or store['ENGINE'].endswith('.DraftModuleStore')
)),
None
)
if mongo_store:
# deepcopy mongo -> split
split_store = copy.deepcopy(mongo_store)
# update the ENGINE and NAME fields
split_store['ENGINE'] = 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore'
split_store['NAME'] = 'split'
# add split to the end of the list
mixed_stores.append(split_store)
return module_store_setting return module_store_setting
......
...@@ -37,10 +37,11 @@ from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceVa ...@@ -37,10 +37,11 @@ from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceVa
from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, ReferentialIntegrityError from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
from xblock.core import XBlock from xblock.core import XBlock
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
from opaque_keys.edx.keys import UsageKey, CourseKey from opaque_keys.edx.keys import UsageKey, CourseKey
from xmodule.exceptions import HeartbeatFailure from xmodule.exceptions import HeartbeatFailure
...@@ -354,8 +355,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -354,8 +355,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
""" """
A Mongodb backed ModuleStore A Mongodb backed ModuleStore
""" """
reference_type = SlashSeparatedCourseKey
# TODO (cpennington): Enable non-filesystem filestores # TODO (cpennington): Enable non-filesystem filestores
# pylint: disable=C0103 # pylint: disable=C0103
# pylint: disable=W0201 # pylint: disable=W0201
...@@ -716,7 +715,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -716,7 +715,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
for item in items for item in items
] ]
def get_courses(self): def get_courses(self, **kwargs):
''' '''
Returns a list of course descriptors. Returns a list of course descriptors.
''' '''
...@@ -751,7 +750,16 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -751,7 +750,16 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
return item return item
def get_course(self, course_key, depth=0): def make_course_key(self, org, course, run):
"""
Return a valid :class:`~opaque_keys.edx.keys.CourseKey` for this modulestore
that matches the supplied `org`, `course`, and `run`.
This key may represent a course that doesn't exist in this modulestore.
"""
return CourseLocator(org, course, run, deprecated=True)
def get_course(self, course_key, depth=0, **kwargs):
""" """
Get the course with the given courseid (org/course/run) Get the course with the given courseid (org/course/run)
""" """
...@@ -763,7 +771,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -763,7 +771,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
except ItemNotFoundError: except ItemNotFoundError:
return None return None
def has_course(self, course_key, ignore_case=False): def has_course(self, course_key, ignore_case=False, **kwargs):
""" """
Returns the course_id of the course if it was found, else None Returns the course_id of the course if it was found, else None
Note: we return the course_id instead of a boolean here since the found course may have Note: we return the course_id instead of a boolean here since the found course may have
...@@ -838,7 +846,15 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -838,7 +846,15 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
for key in ('tag', 'org', 'course', 'category', 'name', 'revision') for key in ('tag', 'org', 'course', 'category', 'name', 'revision')
]) ])
def get_items(self, course_id, settings=None, content=None, key_revision=MongoRevisionKey.published, **kwargs): def get_items(
self,
course_id,
settings=None,
content=None,
key_revision=MongoRevisionKey.published,
qualifiers=None,
**kwargs
):
""" """
Returns: Returns:
list of XModuleDescriptor instances for the matching items within the course with list of XModuleDescriptor instances for the matching items within the course with
...@@ -853,15 +869,15 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -853,15 +869,15 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
Args: Args:
course_id (CourseKey): the course identifier course_id (CourseKey): the course identifier
settings (dict): fields to look for which have settings scope. Follows same syntax settings (dict): fields to look for which have settings scope. Follows same syntax
and rules as kwargs below and rules as qualifiers below
content (dict): fields to look for which have content scope. Follows same syntax and content (dict): fields to look for which have content scope. Follows same syntax and
rules as kwargs below. rules as qualifiers below.
key_revision (str): the revision of the items you're looking for. key_revision (str): the revision of the items you're looking for.
MongoRevisionKey.draft - only returns drafts MongoRevisionKey.draft - only returns drafts
MongoRevisionKey.published (equates to None) - only returns published MongoRevisionKey.published (equates to None) - only returns published
If you want one of each matching xblock but preferring draft to published, call this same method If you want one of each matching xblock but preferring draft to published, call this same method
on the draft modulestore with ModuleStoreEnum.RevisionOption.draft_preferred. on the draft modulestore with ModuleStoreEnum.RevisionOption.draft_preferred.
kwargs (key=value): what to look for within the course. qualifiers (dict): what to look for within the course.
Common qualifiers are ``category`` or any field name. if the target field is a list, Common qualifiers are ``category`` or any field name. if the target field is a list,
then it searches for the given value in the list not list equivalence. then it searches for the given value in the list not list equivalence.
Substring matching pass a regex object. Substring matching pass a regex object.
...@@ -869,20 +885,21 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -869,20 +885,21 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
This modulestore does not allow searching dates by comparison or edited_by, previous_version, This modulestore does not allow searching dates by comparison or edited_by, previous_version,
update_version info. update_version info.
""" """
qualifiers = qualifiers.copy() if qualifiers else {} # copy the qualifiers (destructively manipulated here)
query = self._course_key_to_son(course_id) query = self._course_key_to_son(course_id)
query['_id.revision'] = key_revision query['_id.revision'] = key_revision
for field in ['category', 'name']: for field in ['category', 'name']:
if field in kwargs: if field in qualifiers:
query['_id.' + field] = kwargs.pop(field) query['_id.' + field] = qualifiers.pop(field)
for key, value in (settings or {}).iteritems(): for key, value in (settings or {}).iteritems():
query['metadata.' + key] = value query['metadata.' + key] = value
for key, value in (content or {}).iteritems(): for key, value in (content or {}).iteritems():
query['definition.data.' + key] = value query['definition.data.' + key] = value
if 'children' in kwargs: if 'children' in qualifiers:
query['definition.children'] = kwargs.pop('children') query['definition.children'] = qualifiers.pop('children')
query.update(kwargs) query.update(qualifiers)
items = self.collection.find( items = self.collection.find(
query, query,
sort=[SORT_REVISION_FAVOR_DRAFT], sort=[SORT_REVISION_FAVOR_DRAFT],
...@@ -919,10 +936,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -919,10 +936,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
]) ])
courses = self.collection.find(course_search_location, fields=('_id')) courses = self.collection.find(course_search_location, fields=('_id'))
if courses.count() > 0: if courses.count() > 0:
raise InvalidLocationError( raise DuplicateCourseError(course_id, courses[0]['_id'])
"There are already courses with the given org and course id: {}".format([
course['_id'] for course in courses
]))
location = course_id.make_usage_key('course', course_id.run) location = course_id.make_usage_key('course', course_id.run)
course = self.create_xmodule( course = self.create_xmodule(
...@@ -1253,7 +1267,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -1253,7 +1267,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
""" """
return ModuleStoreEnum.Type.mongo return ModuleStoreEnum.Type.mongo
def get_orphans(self, course_key): def get_orphans(self, course_key, **kwargs):
""" """
Return an array of all of the locations for orphans in the course. Return an array of all of the locations for orphans in the course.
""" """
...@@ -1274,7 +1288,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -1274,7 +1288,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
item_locs -= all_reachable item_locs -= all_reachable
return [course_key.make_usage_key_from_deprecated_string(item_loc) for item_loc in item_locs] return [course_key.make_usage_key_from_deprecated_string(item_loc) for item_loc in item_locs]
def get_courses_for_wiki(self, wiki_slug): def get_courses_for_wiki(self, wiki_slug, **kwargs):
""" """
Return the list of courses which use this wiki_slug Return the list of courses which use this wiki_slug
:param wiki_slug: the course wiki root slug :param wiki_slug: the course wiki root slug
......
...@@ -47,7 +47,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -47,7 +47,7 @@ class DraftModuleStore(MongoModuleStore):
This module also includes functionality to promote DRAFT modules (and their children) This module also includes functionality to promote DRAFT modules (and their children)
to published modules. to published modules.
""" """
def get_item(self, usage_key, depth=0, revision=None): def get_item(self, usage_key, depth=0, revision=None, **kwargs):
""" """
Returns an XModuleDescriptor instance for the item at usage_key. Returns an XModuleDescriptor instance for the item at usage_key.
...@@ -155,7 +155,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -155,7 +155,7 @@ class DraftModuleStore(MongoModuleStore):
course_query = self._course_key_to_son(course_key) course_query = self._course_key_to_son(course_key)
self.collection.remove(course_query, multi=True) self.collection.remove(course_query, multi=True)
def clone_course(self, source_course_id, dest_course_id, user_id, fields=None): def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs):
""" """
Only called if cloning within this store or if env doesn't set up mixed. Only called if cloning within this store or if env doesn't set up mixed.
* copy the courseware * copy the courseware
...@@ -331,11 +331,6 @@ class DraftModuleStore(MongoModuleStore): ...@@ -331,11 +331,6 @@ class DraftModuleStore(MongoModuleStore):
returns only Published items returns only Published items
if the branch setting is ModuleStoreEnum.Branch.draft_preferred, if the branch setting is ModuleStoreEnum.Branch.draft_preferred,
returns either Draft or Published, preferring Draft items. returns either Draft or Published, preferring Draft items.
kwargs (key=value): what to look for within the course.
Common qualifiers are ``category`` or any field name. if the target field is a list,
then it searches for the given value in the list not list equivalence.
Substring matching pass a regex object.
``name`` is another commonly provided key (Location based stores)
""" """
def base_get_items(key_revision): def base_get_items(key_revision):
return super(DraftModuleStore, self).get_items(course_key, key_revision=key_revision, **kwargs) return super(DraftModuleStore, self).get_items(course_key, key_revision=key_revision, **kwargs)
...@@ -439,7 +434,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -439,7 +434,7 @@ class DraftModuleStore(MongoModuleStore):
# convert the subtree using the original item as the root # convert the subtree using the original item as the root
self._breadth_first(convert_item, [location]) self._breadth_first(convert_item, [location])
def update_item(self, xblock, user_id, allow_not_found=False, force=False, isPublish=False): def update_item(self, xblock, user_id, allow_not_found=False, force=False, isPublish=False, **kwargs):
""" """
See superclass doc. See superclass doc.
In addition to the superclass's behavior, this method converts the unit to draft if it's not In addition to the superclass's behavior, this method converts the unit to draft if it's not
...@@ -616,7 +611,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -616,7 +611,7 @@ class DraftModuleStore(MongoModuleStore):
else: else:
return False return False
def publish(self, location, user_id): def publish(self, location, user_id, **kwargs):
""" """
Publish the subtree rooted at location to the live course and remove the drafts. Publish the subtree rooted at location to the live course and remove the drafts.
Such publishing may cause the deletion of previously published but subsequently deleted Such publishing may cause the deletion of previously published but subsequently deleted
...@@ -690,7 +685,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -690,7 +685,7 @@ class DraftModuleStore(MongoModuleStore):
self.collection.remove({'_id': {'$in': to_be_deleted}}) self.collection.remove({'_id': {'$in': to_be_deleted}})
return self.get_item(as_published(location)) return self.get_item(as_published(location))
def unpublish(self, location, user_id): def unpublish(self, location, user_id, **kwargs):
""" """
Turn the published version into a draft, removing the published version. Turn the published version into a draft, removing the published version.
......
...@@ -25,7 +25,9 @@ class SplitMigrator(object): ...@@ -25,7 +25,9 @@ class SplitMigrator(object):
self.split_modulestore = split_modulestore self.split_modulestore = split_modulestore
self.source_modulestore = source_modulestore self.source_modulestore = source_modulestore
def migrate_mongo_course(self, source_course_key, user_id, new_org=None, new_course=None, new_run=None, fields=None): def migrate_mongo_course(
self, source_course_key, user_id, new_org=None, new_course=None, new_run=None, fields=None, **kwargs
):
""" """
Create a new course in split_mongo representing the published and draft versions of the course from the Create a new course in split_mongo representing the published and draft versions of the course from the
original mongo store. And return the new CourseLocator original mongo store. And return the new CourseLocator
...@@ -43,7 +45,7 @@ class SplitMigrator(object): ...@@ -43,7 +45,7 @@ class SplitMigrator(object):
# locations are in location, children, conditionals, course.tab # locations are in location, children, conditionals, course.tab
# create the course: set fields to explicitly_set for each scope, id_root = new_course_locator, master_branch = 'production' # create the course: set fields to explicitly_set for each scope, id_root = new_course_locator, master_branch = 'production'
original_course = self.source_modulestore.get_course(source_course_key) original_course = self.source_modulestore.get_course(source_course_key, **kwargs)
if new_org is None: if new_org is None:
new_org = source_course_key.org new_org = source_course_key.org
...@@ -60,17 +62,20 @@ class SplitMigrator(object): ...@@ -60,17 +62,20 @@ class SplitMigrator(object):
new_org, new_course, new_run, user_id, new_org, new_course, new_run, user_id,
fields=new_fields, fields=new_fields,
master_branch=ModuleStoreEnum.BranchName.published, master_branch=ModuleStoreEnum.BranchName.published,
**kwargs
) )
with self.split_modulestore.bulk_write_operations(new_course.id): with self.split_modulestore.bulk_write_operations(new_course.id):
self._copy_published_modules_to_course(new_course, original_course.location, source_course_key, user_id) self._copy_published_modules_to_course(
new_course, original_course.location, source_course_key, user_id, **kwargs
)
# create a new version for the drafts # create a new version for the drafts
with self.split_modulestore.bulk_write_operations(new_course.id): with self.split_modulestore.bulk_write_operations(new_course.id):
self._add_draft_modules_to_course(new_course.location, source_course_key, user_id) self._add_draft_modules_to_course(new_course.location, source_course_key, user_id, **kwargs)
return new_course.id return new_course.id
def _copy_published_modules_to_course(self, new_course, old_course_loc, source_course_key, user_id): def _copy_published_modules_to_course(self, new_course, old_course_loc, source_course_key, user_id, **kwargs):
""" """
Copy all of the modules from the 'direct' version of the course to the new split course. Copy all of the modules from the 'direct' version of the course to the new split course.
""" """
...@@ -79,7 +84,7 @@ class SplitMigrator(object): ...@@ -79,7 +84,7 @@ class SplitMigrator(object):
# iterate over published course elements. Wildcarding rather than descending b/c some elements are orphaned (e.g., # iterate over published course elements. Wildcarding rather than descending b/c some elements are orphaned (e.g.,
# course about pages, conditionals) # course about pages, conditionals)
for module in self.source_modulestore.get_items( for module in self.source_modulestore.get_items(
source_course_key, revision=ModuleStoreEnum.RevisionOption.published_only source_course_key, revision=ModuleStoreEnum.RevisionOption.published_only, **kwargs
): ):
# don't copy the course again. # don't copy the course again.
if module.location != old_course_loc: if module.location != old_course_loc:
...@@ -95,7 +100,8 @@ class SplitMigrator(object): ...@@ -95,7 +100,8 @@ class SplitMigrator(object):
fields=self._get_fields_translate_references( fields=self._get_fields_translate_references(
module, course_version_locator, new_course.location.block_id module, course_version_locator, new_course.location.block_id
), ),
continue_version=True continue_version=True,
**kwargs
) )
# after done w/ published items, add version for DRAFT pointing to the published structure # after done w/ published items, add version for DRAFT pointing to the published structure
index_info = self.split_modulestore.get_course_index_info(course_version_locator) index_info = self.split_modulestore.get_course_index_info(course_version_locator)
...@@ -107,7 +113,7 @@ class SplitMigrator(object): ...@@ -107,7 +113,7 @@ class SplitMigrator(object):
# children which meant some pointers were to non-existent locations in 'direct' # children which meant some pointers were to non-existent locations in 'direct'
self.split_modulestore.internal_clean_children(course_version_locator) self.split_modulestore.internal_clean_children(course_version_locator)
def _add_draft_modules_to_course(self, published_course_usage_key, source_course_key, user_id): def _add_draft_modules_to_course(self, published_course_usage_key, source_course_key, user_id, **kwargs):
""" """
update each draft. Create any which don't exist in published and attach to their parents. update each draft. Create any which don't exist in published and attach to their parents.
""" """
...@@ -117,11 +123,13 @@ class SplitMigrator(object): ...@@ -117,11 +123,13 @@ class SplitMigrator(object):
# to prevent race conditions of grandchilden being added before their parents and thus having no parent to # to prevent race conditions of grandchilden being added before their parents and thus having no parent to
# add to # add to
awaiting_adoption = {} awaiting_adoption = {}
for module in self.source_modulestore.get_items(source_course_key, revision=ModuleStoreEnum.RevisionOption.draft_only): for module in self.source_modulestore.get_items(
source_course_key, revision=ModuleStoreEnum.RevisionOption.draft_only, **kwargs
):
new_locator = new_draft_course_loc.make_usage_key(module.category, module.location.block_id) new_locator = new_draft_course_loc.make_usage_key(module.category, module.location.block_id)
if self.split_modulestore.has_item(new_locator): if self.split_modulestore.has_item(new_locator):
# was in 'direct' so draft is a new version # was in 'direct' so draft is a new version
split_module = self.split_modulestore.get_item(new_locator) split_module = self.split_modulestore.get_item(new_locator, **kwargs)
# need to remove any no-longer-explicitly-set values and add/update any now set values. # need to remove any no-longer-explicitly-set values and add/update any now set values.
for name, field in split_module.fields.iteritems(): for name, field in split_module.fields.iteritems():
if field.is_set_on(split_module) and not module.fields[name].is_set_on(module): if field.is_set_on(split_module) and not module.fields[name].is_set_on(module):
...@@ -131,7 +139,7 @@ class SplitMigrator(object): ...@@ -131,7 +139,7 @@ class SplitMigrator(object):
).iteritems(): ).iteritems():
field.write_to(split_module, value) field.write_to(split_module, value)
_new_module = self.split_modulestore.update_item(split_module, user_id) _new_module = self.split_modulestore.update_item(split_module, user_id, **kwargs)
else: else:
# only a draft version (aka, 'private'). # only a draft version (aka, 'private').
_new_module = self.split_modulestore.create_item( _new_module = self.split_modulestore.create_item(
...@@ -140,22 +148,23 @@ class SplitMigrator(object): ...@@ -140,22 +148,23 @@ class SplitMigrator(object):
block_id=new_locator.block_id, block_id=new_locator.block_id,
fields=self._get_fields_translate_references( fields=self._get_fields_translate_references(
module, new_draft_course_loc, published_course_usage_key.block_id module, new_draft_course_loc, published_course_usage_key.block_id
) ),
**kwargs
) )
awaiting_adoption[module.location] = new_locator awaiting_adoption[module.location] = new_locator
for draft_location, new_locator in awaiting_adoption.iteritems(): for draft_location, new_locator in awaiting_adoption.iteritems():
parent_loc = self.source_modulestore.get_parent_location( parent_loc = self.source_modulestore.get_parent_location(
draft_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred draft_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred, **kwargs
) )
if parent_loc is None: if parent_loc is None:
log.warn(u'No parent found in source course for %s', draft_location) log.warn(u'No parent found in source course for %s', draft_location)
continue continue
old_parent = self.source_modulestore.get_item(parent_loc) old_parent = self.source_modulestore.get_item(parent_loc, **kwargs)
split_parent_loc = new_draft_course_loc.make_usage_key( split_parent_loc = new_draft_course_loc.make_usage_key(
parent_loc.category, parent_loc.category,
parent_loc.block_id if parent_loc.category != 'course' else published_course_usage_key.block_id parent_loc.block_id if parent_loc.category != 'course' else published_course_usage_key.block_id
) )
new_parent = self.split_modulestore.get_item(split_parent_loc) new_parent = self.split_modulestore.get_item(split_parent_loc, **kwargs)
# this only occurs if the parent was also awaiting adoption: skip this one, go to next # this only occurs if the parent was also awaiting adoption: skip this one, go to next
if any(new_locator == child.version_agnostic() for child in new_parent.children): if any(new_locator == child.version_agnostic() for child in new_parent.children):
continue continue
......
...@@ -53,7 +53,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -53,7 +53,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
self.default_class = default_class self.default_class = default_class
self.local_modules = {} self.local_modules = {}
def _load_item(self, block_id, course_entry_override=None): def _load_item(self, block_id, course_entry_override=None, **kwargs):
if isinstance(block_id, BlockUsageLocator): if isinstance(block_id, BlockUsageLocator):
if isinstance(block_id.block_id, LocalId): if isinstance(block_id.block_id, LocalId):
try: try:
...@@ -77,7 +77,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -77,7 +77,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
raise ItemNotFoundError(block_id) raise ItemNotFoundError(block_id)
class_ = self.load_block_type(json_data.get('category')) class_ = self.load_block_type(json_data.get('category'))
return self.xblock_from_json(class_, block_id, json_data, course_entry_override) return self.xblock_from_json(class_, block_id, json_data, course_entry_override, **kwargs)
# xblock's runtime does not always pass enough contextual information to figure out # xblock's runtime does not always pass enough contextual information to figure out
# which named container (course x branch) or which parent is requesting an item. Because split allows # which named container (course x branch) or which parent is requesting an item. Because split allows
...@@ -90,7 +90,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -90,7 +90,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# low; thus, the course_entry is most likely correct. If the thread is looking at > 1 named container # low; thus, the course_entry is most likely correct. If the thread is looking at > 1 named container
# pointing to the same structure, the access is likely to be chunky enough that the last known container # pointing to the same structure, the access is likely to be chunky enough that the last known container
# is the intended one when not given a course_entry_override; thus, the caching of the last branch/course id. # is the intended one when not given a course_entry_override; thus, the caching of the last branch/course id.
def xblock_from_json(self, class_, block_id, json_data, course_entry_override=None): def xblock_from_json(self, class_, block_id, json_data, course_entry_override=None, **kwargs):
if course_entry_override is None: if course_entry_override is None:
course_entry_override = self.course_entry course_entry_override = self.course_entry
else: else:
...@@ -126,6 +126,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -126,6 +126,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
definition, definition,
converted_fields, converted_fields,
json_data.get('_inherited_settings'), json_data.get('_inherited_settings'),
**kwargs
) )
field_data = KvsFieldData(kvs) field_data = KvsFieldData(kvs)
...@@ -151,6 +152,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -151,6 +152,10 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
edit_info = json_data.get('edit_info', {}) edit_info = json_data.get('edit_info', {})
module.edited_by = edit_info.get('edited_by') module.edited_by = edit_info.get('edited_by')
module.edited_on = edit_info.get('edited_on') module.edited_on = edit_info.get('edited_on')
module.subtree_edited_by = None # TODO - addressed with LMS-11183
module.subtree_edited_on = None # TODO - addressed with LMS-11183
module.published_by = None # TODO - addressed with LMS-11184
module.published_date = None # TODO - addressed with LMS-11184
module.previous_version = edit_info.get('previous_version') module.previous_version = edit_info.get('previous_version')
module.update_version = edit_info.get('update_version') module.update_version = edit_info.get('update_version')
module.source_version = edit_info.get('source_version', None) module.source_version = edit_info.get('source_version', None)
......
...@@ -15,45 +15,52 @@ class SplitMongoKVS(InheritanceKeyValueStore): ...@@ -15,45 +15,52 @@ class SplitMongoKVS(InheritanceKeyValueStore):
known to the MongoModuleStore (data, children, and metadata) known to the MongoModuleStore (data, children, and metadata)
""" """
def __init__(self, definition, fields, inherited_settings): def __init__(self, definition, initial_values, inherited_settings, **kwargs):
""" """
:param definition: either a lazyloader or definition id for the definition :param definition: either a lazyloader or definition id for the definition
:param fields: a dictionary of the locally set fields :param initial_values: a dictionary of the locally set values
:param inherited_settings: the json value of each inheritable field from above this. :param inherited_settings: the json value of each inheritable field from above this.
Note, local fields may override and disagree w/ this b/c this says what the value Note, local fields may override and disagree w/ this b/c this says what the value
should be if the field is undefined. should be if the field is undefined.
""" """
# deepcopy so that manipulations of fields does not pollute the source # deepcopy so that manipulations of fields does not pollute the source
super(SplitMongoKVS, self).__init__(copy.deepcopy(fields), inherited_settings) super(SplitMongoKVS, self).__init__(copy.deepcopy(initial_values), inherited_settings)
self._definition = definition # either a DefinitionLazyLoader or the db id of the definition. self._definition = definition # either a DefinitionLazyLoader or the db id of the definition.
# if the db id, then the definition is presumed to be loaded into _fields # if the db id, then the definition is presumed to be loaded into _fields
# a decorator function for field values (to be called when a field is accessed)
self.field_decorator = kwargs.get('field_decorator', lambda x: x)
def get(self, key): def get(self, key):
# simplest case, field is directly set # load the field, if needed
if key.field_name not in self._fields:
# parent undefined in editing runtime (I think)
if key.scope == Scope.parent:
# see STUD-624. Right now copies MongoKeyValueStore.get's behavior of returning None
return None
if key.scope == Scope.children:
# didn't find children in _fields; so, see if there's a default
raise KeyError()
elif key.scope == Scope.settings:
# get default which may be the inherited value
raise KeyError()
elif key.scope == Scope.content:
if isinstance(self._definition, DefinitionLazyLoader):
self._load_definition()
else:
raise KeyError()
else:
raise InvalidScopeError(key)
if key.field_name in self._fields: if key.field_name in self._fields:
return self._fields[key.field_name] field_value = self._fields[key.field_name]
# parent undefined in editing runtime (I think) # return the "decorated" field value
if key.scope == Scope.parent: return self.field_decorator(field_value)
# see STUD-624. Right now copies MongoKeyValueStore.get's behavior of returning None
return None return None
if key.scope == Scope.children:
# didn't find children in _fields; so, see if there's a default
raise KeyError()
elif key.scope == Scope.settings:
# get default which may be the inherited value
raise KeyError()
elif key.scope == Scope.content:
if isinstance(self._definition, DefinitionLazyLoader):
self._load_definition()
if key.field_name in self._fields:
return self._fields[key.field_name]
raise KeyError()
else:
raise InvalidScopeError(key)
def set(self, key, value): def set(self, key, value):
# handle any special cases # handle any special cases
......
...@@ -9,7 +9,8 @@ from tempfile import mkdtemp ...@@ -9,7 +9,8 @@ from tempfile import mkdtemp
import path import path
import shutil import shutil
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation from opaque_keys.edx.locator import CourseLocator, AssetLocator
from opaque_keys.edx.keys import AssetKey
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from xmodule.contentstore.mongo import MongoContentStore from xmodule.contentstore.mongo import MongoContentStore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
...@@ -41,13 +42,13 @@ class TestContentstore(unittest.TestCase): ...@@ -41,13 +42,13 @@ class TestContentstore(unittest.TestCase):
Restores deprecated values Restores deprecated values
""" """
if cls.asset_deprecated is not None: if cls.asset_deprecated is not None:
setattr(AssetLocation, 'deprecated', cls.asset_deprecated) setattr(AssetLocator, 'deprecated', cls.asset_deprecated)
else: else:
delattr(AssetLocation, 'deprecated') delattr(AssetLocator, 'deprecated')
if cls.ssck_deprecated is not None: if cls.ssck_deprecated is not None:
setattr(SlashSeparatedCourseKey, 'deprecated', cls.ssck_deprecated) setattr(CourseLocator, 'deprecated', cls.ssck_deprecated)
else: else:
delattr(SlashSeparatedCourseKey, 'deprecated') delattr(CourseLocator, 'deprecated')
return super(TestContentstore, cls).tearDownClass() return super(TestContentstore, cls).tearDownClass()
def set_up_assets(self, deprecated): def set_up_assets(self, deprecated):
...@@ -59,11 +60,11 @@ class TestContentstore(unittest.TestCase): ...@@ -59,11 +60,11 @@ class TestContentstore(unittest.TestCase):
self.contentstore = MongoContentStore(HOST, DB, port=PORT) self.contentstore = MongoContentStore(HOST, DB, port=PORT)
self.addCleanup(self.contentstore._drop_database) # pylint: disable=protected-access self.addCleanup(self.contentstore._drop_database) # pylint: disable=protected-access
setattr(AssetLocation, 'deprecated', deprecated) setattr(AssetLocator, 'deprecated', deprecated)
setattr(SlashSeparatedCourseKey, 'deprecated', deprecated) setattr(CourseLocator, 'deprecated', deprecated)
self.course1_key = SlashSeparatedCourseKey('test', 'asset_test', '2014_07') self.course1_key = CourseLocator('test', 'asset_test', '2014_07')
self.course2_key = SlashSeparatedCourseKey('test', 'asset_test2', '2014_07') self.course2_key = CourseLocator('test', 'asset_test2', '2014_07')
self.course1_files = ['contains.sh', 'picture1.jpg', 'picture2.jpg'] self.course1_files = ['contains.sh', 'picture1.jpg', 'picture2.jpg']
self.course2_files = ['picture1.jpg', 'picture3.jpg', 'door_2.ogg'] self.course2_files = ['picture1.jpg', 'picture3.jpg', 'door_2.ogg']
...@@ -154,13 +155,13 @@ class TestContentstore(unittest.TestCase): ...@@ -154,13 +155,13 @@ class TestContentstore(unittest.TestCase):
course1_assets, count = self.contentstore.get_all_content_for_course(self.course1_key) course1_assets, count = self.contentstore.get_all_content_for_course(self.course1_key)
self.assertEqual(count, len(self.course1_files), course1_assets) self.assertEqual(count, len(self.course1_files), course1_assets)
for asset in course1_assets: for asset in course1_assets:
parsed = AssetLocation.from_deprecated_string(asset['filename']) parsed = AssetKey.from_string(asset['filename'])
self.assertIn(parsed.name, self.course1_files) self.assertIn(parsed.name, self.course1_files)
course1_assets, __ = self.contentstore.get_all_content_for_course(self.course1_key, 1, 1) course1_assets, __ = self.contentstore.get_all_content_for_course(self.course1_key, 1, 1)
self.assertEqual(len(course1_assets), 1, course1_assets) self.assertEqual(len(course1_assets), 1, course1_assets)
fake_course = SlashSeparatedCourseKey('test', 'fake', 'non') fake_course = CourseLocator('test', 'fake', 'non')
course_assets, count = self.contentstore.get_all_content_for_course(fake_course) course_assets, count = self.contentstore.get_all_content_for_course(fake_course)
self.assertEqual(count, 0) self.assertEqual(count, 0)
self.assertEqual(course_assets, []) self.assertEqual(course_assets, [])
...@@ -183,7 +184,7 @@ class TestContentstore(unittest.TestCase): ...@@ -183,7 +184,7 @@ class TestContentstore(unittest.TestCase):
copy_all_course_assets copy_all_course_assets
""" """
self.set_up_assets(deprecated) self.set_up_assets(deprecated)
dest_course = SlashSeparatedCourseKey('test', 'destination', 'copy') dest_course = CourseLocator('test', 'destination', 'copy')
self.contentstore.copy_all_course_assets(self.course1_key, dest_course) self.contentstore.copy_all_course_assets(self.course1_key, dest_course)
for filename in self.course1_files: for filename in self.course1_files:
asset_key = self.course1_key.make_asset_key('asset', filename) asset_key = self.course1_key.make_asset_key('asset', filename)
......
...@@ -45,7 +45,6 @@ class ModuleStoreSettingsMigration(TestCase): ...@@ -45,7 +45,6 @@ class ModuleStoreSettingsMigration(TestCase):
"ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore",
"OPTIONS": { "OPTIONS": {
"mappings": {}, "mappings": {},
"reference_type": "Location",
"stores": { "stores": {
"an_old_mongo_store": { "an_old_mongo_store": {
"DOC_STORE_CONFIG": {}, "DOC_STORE_CONFIG": {},
...@@ -77,15 +76,47 @@ class ModuleStoreSettingsMigration(TestCase): ...@@ -77,15 +76,47 @@ class ModuleStoreSettingsMigration(TestCase):
} }
} }
ALREADY_UPDATED_MIXED_CONFIG = {
'default': {
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': {
'mappings': {},
'stores': [
{
'NAME': 'split',
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': {},
'OPTIONS': {
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'fs_root': "fs_root",
'render_template': 'edxmako.shortcuts.render_to_string',
}
},
{
'NAME': 'draft',
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
'DOC_STORE_CONFIG': {},
'OPTIONS': {
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'fs_root': "fs_root",
'render_template': 'edxmako.shortcuts.render_to_string',
}
},
]
}
}
}
def _get_mixed_stores(self, mixed_setting): def _get_mixed_stores(self, mixed_setting):
""" """
Helper for accessing stores in a configuration setting for the Mixed modulestore Helper for accessing stores in a configuration setting for the Mixed modulestore.
""" """
return mixed_setting["default"]["OPTIONS"]["stores"] return mixed_setting["default"]["OPTIONS"]["stores"]
def assertStoreValuesEqual(self, store_setting1, store_setting2): def assertStoreValuesEqual(self, store_setting1, store_setting2):
""" """
Tests whether the fields in the given store_settings are equal Tests whether the fields in the given store_settings are equal.
""" """
store_fields = ["OPTIONS", "DOC_STORE_CONFIG"] store_fields = ["OPTIONS", "DOC_STORE_CONFIG"]
for field in store_fields: for field in store_fields:
...@@ -108,26 +139,56 @@ class ModuleStoreSettingsMigration(TestCase): ...@@ -108,26 +139,56 @@ class ModuleStoreSettingsMigration(TestCase):
return new_mixed_setting, new_stores[0] return new_mixed_setting, new_stores[0]
def is_split_configured(self, mixed_setting):
"""
Tests whether the split module store is configured in the given setting.
"""
stores = self._get_mixed_stores(mixed_setting)
split_settings = [store for store in stores if store['ENGINE'].endswith('.DraftVersioningModuleStore')]
if len(split_settings):
# there should only be one setting for split
self.assertEquals(len(split_settings), 1)
# verify name
self.assertEquals(split_settings[0]['NAME'], 'split')
# verify split config settings equal those of mongo
self.assertStoreValuesEqual(
split_settings[0],
next((store for store in stores if 'DraftModuleStore' in store['ENGINE']), None)
)
return len(split_settings) > 0
def test_convert_into_mixed(self): def test_convert_into_mixed(self):
old_setting = self.OLD_CONFIG old_setting = self.OLD_CONFIG
_, new_default_store_setting = self.assertMigrated(old_setting) new_mixed_setting, new_default_store_setting = self.assertMigrated(old_setting)
self.assertStoreValuesEqual(new_default_store_setting, old_setting["default"]) self.assertStoreValuesEqual(new_default_store_setting, old_setting["default"])
self.assertEqual(new_default_store_setting["ENGINE"], old_setting["default"]["ENGINE"]) self.assertEqual(new_default_store_setting["ENGINE"], old_setting["default"]["ENGINE"])
self.assertFalse(self.is_split_configured(new_mixed_setting))
def test_convert_from_old_mongo_to_draft_store(self): def test_convert_from_old_mongo_to_draft_store(self):
old_setting = self.OLD_CONFIG_WITH_DIRECT_MONGO old_setting = self.OLD_CONFIG_WITH_DIRECT_MONGO
_, new_default_store_setting = self.assertMigrated(old_setting) new_mixed_setting, new_default_store_setting = self.assertMigrated(old_setting)
self.assertStoreValuesEqual(new_default_store_setting, old_setting["default"]) self.assertStoreValuesEqual(new_default_store_setting, old_setting["default"])
self.assertEqual(new_default_store_setting["ENGINE"], "xmodule.modulestore.mongo.draft.DraftModuleStore") self.assertEqual(new_default_store_setting["ENGINE"], "xmodule.modulestore.mongo.draft.DraftModuleStore")
self.assertTrue(self.is_split_configured(new_mixed_setting))
def test_convert_from_dict_to_list(self): def test_convert_from_dict_to_list(self):
old_mixed_setting = self.OLD_MIXED_CONFIG_WITH_DICT old_mixed_setting = self.OLD_MIXED_CONFIG_WITH_DICT
new_mixed_setting, new_default_store_setting = self.assertMigrated(old_mixed_setting) new_mixed_setting, new_default_store_setting = self.assertMigrated(old_mixed_setting)
self.assertEqual(new_default_store_setting["ENGINE"], "the_default_store") self.assertEqual(new_default_store_setting["ENGINE"], "the_default_store")
self.assertTrue(self.is_split_configured(new_mixed_setting))
# compare each store configured in mixed # exclude split when comparing old and new, since split was added as part of the migration
new_stores = [store for store in self._get_mixed_stores(new_mixed_setting) if store['NAME'] != 'split']
old_stores = self._get_mixed_stores(self.OLD_MIXED_CONFIG_WITH_DICT) old_stores = self._get_mixed_stores(self.OLD_MIXED_CONFIG_WITH_DICT)
new_stores = self._get_mixed_stores(new_mixed_setting)
# compare each store configured in mixed
self.assertEqual(len(new_stores), len(old_stores)) self.assertEqual(len(new_stores), len(old_stores))
for new_store_setting in self._get_mixed_stores(new_mixed_setting): for new_store in new_stores:
self.assertStoreValuesEqual(new_store_setting, old_stores[new_store_setting['NAME']]) self.assertStoreValuesEqual(new_store, old_stores[new_store['NAME']])
def test_no_conversion(self):
# make sure there is no migration done on an already updated config
old_mixed_setting = self.ALREADY_UPDATED_MIXED_CONFIG
new_mixed_setting, new_default_store_setting = self.assertMigrated(old_mixed_setting)
self.assertTrue(self.is_split_configured(new_mixed_setting))
self.assertEquals(old_mixed_setting, new_mixed_setting)
...@@ -151,7 +151,7 @@ class TestMigration(SplitWMongoCourseBoostrapper): ...@@ -151,7 +151,7 @@ class TestMigration(SplitWMongoCourseBoostrapper):
# grab the detached items to compare they should be in both published and draft # grab the detached items to compare they should be in both published and draft
for category in ['conditional', 'about', 'course_info', 'static_tab']: for category in ['conditional', 'about', 'course_info', 'static_tab']:
for conditional in presplit.get_items(self.old_course_key, category=category): for conditional in presplit.get_items(self.old_course_key, qualifiers={'category': category}):
locator = new_course_key.make_usage_key(category, conditional.location.block_id) locator = new_course_key.make_usage_key(category, conditional.location.block_id)
self.compare_dags(presplit, conditional, self.split_mongo.get_item(locator), published) self.compare_dags(presplit, conditional, self.split_mongo.get_item(locator), published)
......
...@@ -895,18 +895,18 @@ class SplitModuleItemTests(SplitModuleTest): ...@@ -895,18 +895,18 @@ class SplitModuleItemTests(SplitModuleTest):
self.assertEqual(len(matches), 6) self.assertEqual(len(matches), 6)
matches = modulestore().get_items(locator) matches = modulestore().get_items(locator)
self.assertEqual(len(matches), 6) self.assertEqual(len(matches), 6)
matches = modulestore().get_items(locator, category='chapter') matches = modulestore().get_items(locator, qualifiers={'category': 'chapter'})
self.assertEqual(len(matches), 3) self.assertEqual(len(matches), 3)
matches = modulestore().get_items(locator, category='garbage') matches = modulestore().get_items(locator, qualifiers={'category': 'garbage'})
self.assertEqual(len(matches), 0) self.assertEqual(len(matches), 0)
matches = modulestore().get_items( matches = modulestore().get_items(
locator, locator,
category='chapter', qualifiers={'category': 'chapter'},
settings={'display_name': re.compile(r'Hera')}, settings={'display_name': re.compile(r'Hera')},
) )
self.assertEqual(len(matches), 2) self.assertEqual(len(matches), 2)
matches = modulestore().get_items(locator, children='chapter2') matches = modulestore().get_items(locator, qualifiers={'children': 'chapter2'})
self.assertEqual(len(matches), 1) self.assertEqual(len(matches), 1)
self.assertEqual(matches[0].location.block_id, 'head12345') self.assertEqual(matches[0].location.block_id, 'head12345')
...@@ -1324,7 +1324,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -1324,7 +1324,7 @@ class TestItemCrud(SplitModuleTest):
reusable_location = course.id.version_agnostic().for_branch(BRANCH_NAME_DRAFT) reusable_location = course.id.version_agnostic().for_branch(BRANCH_NAME_DRAFT)
# delete a leaf # delete a leaf
problems = modulestore().get_items(reusable_location, category='problem') problems = modulestore().get_items(reusable_location, qualifiers={'category': 'problem'})
locn_to_del = problems[0].location locn_to_del = problems[0].location
new_course_loc = modulestore().delete_item(locn_to_del, self.user_id) new_course_loc = modulestore().delete_item(locn_to_del, self.user_id)
deleted = locn_to_del.version_agnostic() deleted = locn_to_del.version_agnostic()
...@@ -1336,7 +1336,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -1336,7 +1336,7 @@ class TestItemCrud(SplitModuleTest):
self.assertNotEqual(new_course_loc.version_guid, course.location.version_guid) self.assertNotEqual(new_course_loc.version_guid, course.location.version_guid)
# delete a subtree # delete a subtree
nodes = modulestore().get_items(reusable_location, category='chapter') nodes = modulestore().get_items(reusable_location, qualifiers={'category': 'chapter'})
new_course_loc = modulestore().delete_item(nodes[0].location, self.user_id) new_course_loc = modulestore().delete_item(nodes[0].location, self.user_id)
# check subtree # check subtree
......
...@@ -24,6 +24,7 @@ from xmodule.modulestore import ModuleStoreEnum, ModuleStoreReadBase ...@@ -24,6 +24,7 @@ from xmodule.modulestore import ModuleStoreEnum, ModuleStoreReadBase
from xmodule.tabs import CourseTabList from xmodule.tabs import CourseTabList
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location from opaque_keys.edx.locations import SlashSeparatedCourseKey, Location
from opaque_keys.edx.locator import CourseLocator
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
from xblock.runtime import DictKeyValueStore, IdGenerator from xblock.runtime import DictKeyValueStore, IdGenerator
...@@ -403,7 +404,6 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -403,7 +404,6 @@ class XMLModuleStore(ModuleStoreReadBase):
self.default_class = class_ self.default_class = class_
self.parent_trackers = defaultdict(ParentTracker) self.parent_trackers = defaultdict(ParentTracker)
self.reference_type = Location
# All field data will be stored in an inheriting field data. # All field data will be stored in an inheriting field data.
self.field_data = inheriting_field_data(kvs=DictKeyValueStore()) self.field_data = inheriting_field_data(kvs=DictKeyValueStore())
...@@ -700,7 +700,7 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -700,7 +700,7 @@ class XMLModuleStore(ModuleStoreReadBase):
""" """
return usage_key in self.modules[usage_key.course_key] return usage_key in self.modules[usage_key.course_key]
def get_item(self, usage_key, depth=0): def get_item(self, usage_key, depth=0, **kwargs):
""" """
Returns an XBlock instance for the item for this UsageKey. Returns an XBlock instance for the item for this UsageKey.
...@@ -717,7 +717,7 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -717,7 +717,7 @@ class XMLModuleStore(ModuleStoreReadBase):
except KeyError: except KeyError:
raise ItemNotFoundError(usage_key) raise ItemNotFoundError(usage_key)
def get_items(self, course_id, settings=None, content=None, revision=None, **kwargs): def get_items(self, course_id, settings=None, content=None, revision=None, qualifiers=None, **kwargs):
""" """
Returns: Returns:
list of XModuleDescriptor instances for the matching items within the course with list of XModuleDescriptor instances for the matching items within the course with
...@@ -729,10 +729,10 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -729,10 +729,10 @@ class XMLModuleStore(ModuleStoreReadBase):
Args: Args:
course_id (CourseKey): the course identifier course_id (CourseKey): the course identifier
settings (dict): fields to look for which have settings scope. Follows same syntax settings (dict): fields to look for which have settings scope. Follows same syntax
and rules as kwargs below and rules as qualifiers below
content (dict): fields to look for which have content scope. Follows same syntax and content (dict): fields to look for which have content scope. Follows same syntax and
rules as kwargs below. rules as qualifiers below.
kwargs (key=value): what to look for within the course. qualifiers (dict): what to look for within the course.
Common qualifiers are ``category`` or any field name. if the target field is a list, Common qualifiers are ``category`` or any field name. if the target field is a list,
then it searches for the given value in the list not list equivalence. then it searches for the given value in the list not list equivalence.
Substring matching pass a regex object. Substring matching pass a regex object.
...@@ -747,8 +747,9 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -747,8 +747,9 @@ class XMLModuleStore(ModuleStoreReadBase):
items = [] items = []
category = kwargs.pop('category', None) qualifiers = qualifiers.copy() if qualifiers else {} # copy the qualifiers (destructively manipulated here)
name = kwargs.pop('name', None) category = qualifiers.pop('category', None)
name = qualifiers.pop('name', None)
def _block_matches_all(mod_loc, module): def _block_matches_all(mod_loc, module):
if category and mod_loc.category != category: if category and mod_loc.category != category:
...@@ -757,7 +758,7 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -757,7 +758,7 @@ class XMLModuleStore(ModuleStoreReadBase):
return False return False
return all( return all(
self._block_matches(module, fields or {}) self._block_matches(module, fields or {})
for fields in [settings, content, kwargs] for fields in [settings, content, qualifiers]
) )
for mod_loc, module in self.modules[course_id].iteritems(): for mod_loc, module in self.modules[course_id].iteritems():
...@@ -766,7 +767,16 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -766,7 +767,16 @@ class XMLModuleStore(ModuleStoreReadBase):
return items return items
def get_courses(self, depth=0): def make_course_key(self, org, course, run):
"""
Return a valid :class:`~opaque_keys.edx.keys.CourseKey` for this modulestore
that matches the supplied `org`, `course`, and `run`.
This key may represent a course that doesn't exist in this modulestore.
"""
return CourseLocator(org, course, run, deprecated=True)
def get_courses(self, depth=0, **kwargs):
""" """
Returns a list of course descriptors. If there were errors on loading, Returns a list of course descriptors. If there were errors on loading,
some of these may be ErrorDescriptors instead. some of these may be ErrorDescriptors instead.
...@@ -780,7 +790,7 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -780,7 +790,7 @@ class XMLModuleStore(ModuleStoreReadBase):
""" """
return dict((k, self.errored_courses[k].errors) for k in self.errored_courses) return dict((k, self.errored_courses[k].errors) for k in self.errored_courses)
def get_orphans(self, course_key): def get_orphans(self, course_key, **kwargs):
""" """
Get all of the xblocks in the given course which have no parents and are not of types which are Get all of the xblocks in the given course which have no parents and are not of types which are
usually orphaned. NOTE: may include xblocks which still have references via xblocks which don't usually orphaned. NOTE: may include xblocks which still have references via xblocks which don't
...@@ -806,7 +816,7 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -806,7 +816,7 @@ class XMLModuleStore(ModuleStoreReadBase):
""" """
return ModuleStoreEnum.Type.xml return ModuleStoreEnum.Type.xml
def get_courses_for_wiki(self, wiki_slug): def get_courses_for_wiki(self, wiki_slug, **kwargs):
""" """
Return the list of courses which use this wiki_slug Return the list of courses which use this wiki_slug
:param wiki_slug: the course wiki root slug :param wiki_slug: the course wiki root slug
......
...@@ -106,7 +106,7 @@ def export_to_xml(modulestore, contentstore, course_key, root_dir, course_dir): ...@@ -106,7 +106,7 @@ def export_to_xml(modulestore, contentstore, course_key, root_dir, course_dir):
# and index here since the XML modulestore cannot load draft modules # and index here since the XML modulestore cannot load draft modules
draft_verticals = modulestore.get_items( draft_verticals = modulestore.get_items(
course_key, course_key,
category='vertical', qualifiers={'category': 'vertical'},
revision=ModuleStoreEnum.RevisionOption.draft_only revision=ModuleStoreEnum.RevisionOption.draft_only
) )
if len(draft_verticals) > 0: if len(draft_verticals) > 0:
...@@ -144,7 +144,7 @@ def _export_field_content(xblock_item, item_dir): ...@@ -144,7 +144,7 @@ def _export_field_content(xblock_item, item_dir):
def export_extra_content(export_fs, modulestore, course_key, category_type, dirname, file_suffix=''): def export_extra_content(export_fs, modulestore, course_key, category_type, dirname, file_suffix=''):
items = modulestore.get_items(course_key, category=category_type) items = modulestore.get_items(course_key, qualifiers={'category': category_type})
if len(items) > 0: if len(items) > 0:
item_dir = export_fs.makeopendir(dirname) item_dir = export_fs.makeopendir(dirname)
......
...@@ -17,7 +17,7 @@ from .store_utilities import rewrite_nonportable_content_links ...@@ -17,7 +17,7 @@ from .store_utilities import rewrite_nonportable_content_links
import xblock import xblock
from xmodule.tabs import CourseTabList from xmodule.tabs import CourseTabList
from xmodule.modulestore.django import ASSET_IGNORE_REGEX from xmodule.modulestore.django import ASSET_IGNORE_REGEX
from xmodule.modulestore.exceptions import InvalidLocationError from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore.mongo.base import MongoRevisionKey from xmodule.modulestore.mongo.base import MongoRevisionKey
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -174,8 +174,9 @@ def import_from_xml( ...@@ -174,8 +174,9 @@ def import_from_xml(
if create_new_course_if_not_present and not store.has_course(dest_course_id, ignore_case=True): if create_new_course_if_not_present and not store.has_course(dest_course_id, ignore_case=True):
try: try:
store.create_course(dest_course_id.org, dest_course_id.course, dest_course_id.run, user_id) store.create_course(dest_course_id.org, dest_course_id.course, dest_course_id.run, user_id)
except InvalidLocationError: except DuplicateCourseError:
# course w/ same org and course exists # course w/ same org and course exists
# The Mongo modulestore checks *with* the run in has_course, but not in create_course.
log.debug( log.debug(
"Skipping import of course with id, {0}," "Skipping import of course with id, {0},"
"since it collides with an existing one".format(dest_course_id) "since it collides with an existing one".format(dest_course_id)
......
...@@ -13,7 +13,7 @@ class TestDraftModuleStore(TestCase): ...@@ -13,7 +13,7 @@ class TestDraftModuleStore(TestCase):
store = modulestore() store = modulestore()
# fix was to allow get_items() to take the course_id parameter # fix was to allow get_items() to take the course_id parameter
store.get_items(SlashSeparatedCourseKey('a', 'b', 'c'), category='vertical') store.get_items(SlashSeparatedCourseKey('a', 'b', 'c'), qualifiers={'category': 'vertical'})
# test success is just getting through the above statement. # test success is just getting through the above statement.
# The bug was that 'course_id' argument was # The bug was that 'course_id' argument was
......
...@@ -163,7 +163,7 @@ class TestDraftModuleStore(ModuleStoreTestCase): ...@@ -163,7 +163,7 @@ class TestDraftModuleStore(ModuleStoreTestCase):
store = modulestore() store = modulestore()
# fix was to allow get_items() to take the course_id parameter # fix was to allow get_items() to take the course_id parameter
store.get_items(SlashSeparatedCourseKey('abc', 'def', 'ghi'), category='vertical') store.get_items(SlashSeparatedCourseKey('abc', 'def', 'ghi'), qualifiers={'category': 'vertical'})
# test success is just getting through the above statement. # test success is just getting through the above statement.
# The bug was that 'course_id' argument was # The bug was that 'course_id' argument was
......
...@@ -463,7 +463,7 @@ def jump_to_id(request, course_id, module_id): ...@@ -463,7 +463,7 @@ def jump_to_id(request, course_id, module_id):
passed in. This assumes that id is unique within the course_id namespace passed in. This assumes that id is unique within the course_id namespace
""" """
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
items = modulestore().get_items(course_key, name=module_id) items = modulestore().get_items(course_key, qualifiers={'name': module_id})
if len(items) == 0: if len(items) == 0:
raise Http404( raise Http404(
...@@ -937,7 +937,7 @@ def get_course_lti_endpoints(request, course_id): ...@@ -937,7 +937,7 @@ def get_course_lti_endpoints(request, course_id):
anonymous_user = AnonymousUser() anonymous_user = AnonymousUser()
anonymous_user.known = False # make these "noauth" requests like module_render.handle_xblock_callback_noauth anonymous_user.known = False # make these "noauth" requests like module_render.handle_xblock_callback_noauth
lti_descriptors = modulestore().get_items(course.id, category='lti') lti_descriptors = modulestore().get_items(course.id, qualifiers={'category': 'lti'})
lti_noauth_modules = [ lti_noauth_modules = [
get_module_for_descriptor( get_module_for_descriptor(
......
...@@ -57,7 +57,7 @@ def has_forum_access(uname, course_id, rolename): ...@@ -57,7 +57,7 @@ def has_forum_access(uname, course_id, rolename):
def _get_discussion_modules(course): def _get_discussion_modules(course):
all_modules = modulestore().get_items(course.id, category='discussion') all_modules = modulestore().get_items(course.id, qualifiers={'category': 'discussion'})
def has_required_keys(module): def has_required_keys(module):
for key in ('discussion_id', 'discussion_category', 'discussion_target'): for key in ('discussion_id', 'discussion_category', 'discussion_target'):
......
...@@ -92,7 +92,7 @@ def find_peer_grading_module(course): ...@@ -92,7 +92,7 @@ def find_peer_grading_module(course):
problem_url = "" problem_url = ""
# Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs. # Get the peer grading modules currently in the course. Explicitly specify the course id to avoid issues with different runs.
items = modulestore().get_items(course.id, category='peergrading') items = modulestore().get_items(course.id, qualifiers={'category': 'peergrading'})
# See if any of the modules are centralized modules (ie display info from multiple problems) # See if any of the modules are centralized modules (ie display info from multiple problems)
items = [i for i in items if not getattr(i, "use_for_single_location", True)] items = [i for i in items if not getattr(i, "use_for_single_location", True)]
# Loop through all potential peer grading modules, and find the first one that has a path to it. # Loop through all potential peer grading modules, and find the first one that has a path to it.
......
...@@ -51,7 +51,6 @@ ...@@ -51,7 +51,6 @@
"ENGINE": "xmodule.modulestore.mixed.MixedModuleStore", "ENGINE": "xmodule.modulestore.mixed.MixedModuleStore",
"OPTIONS": { "OPTIONS": {
"mappings": {}, "mappings": {},
"reference_type": "Location",
"stores": [ "stores": [
{ {
"NAME": "draft", "NAME": "draft",
......
...@@ -504,7 +504,6 @@ MODULESTORE = { ...@@ -504,7 +504,6 @@ MODULESTORE = {
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': { 'OPTIONS': {
'mappings': {}, 'mappings': {},
'reference_type': 'Location',
'stores': [ 'stores': [
{ {
'NAME': 'draft', 'NAME': 'draft',
...@@ -524,6 +523,16 @@ MODULESTORE = { ...@@ -524,6 +523,16 @@ MODULESTORE = {
'default_class': 'xmodule.hidden_module.HiddenDescriptor', 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
} }
}, },
{
'NAME': 'split',
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': {
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'fs_root': DATA_DIR,
'render_template': 'edxmako.shortcuts.render_to_string',
}
},
] ]
} }
} }
......
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