Commit 010905eb by Don Mitchell

RESTful api for getting course listing and opening course in studio.

Pattern for how to do refactoring from locations to locators and from
old style urls to restful ones.
parent fd49f092
#=======================================================================================================================
#
# This code is somewhat duplicative of access.py in the LMS. We will unify the code as a separate story
# but this implementation should be data compatible with the LMS implementation
#
#=======================================================================================================================
from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied
from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.locator import CourseLocator, Locator
'''
This code is somewhat duplicative of access.py in the LMS. We will unify the code as a separate story
but this implementation should be data compatible with the LMS implementation
'''
# define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes
INSTRUCTOR_ROLE_NAME = 'instructor'
......@@ -22,16 +25,22 @@ COURSE_CREATOR_GROUP_NAME = "course_creator_group"
def get_course_groupname_for_role(location, role):
loc = Location(location)
location = Locator.to_locator_or_location(location)
# hack: check for existence of a group name in the legacy LMS format <role>_<course>
# if it exists, then use that one, otherwise use a <role>_<course_id> which contains
# more information
groupname = '{0}_{1}'.format(role, loc.course)
if len(Group.objects.filter(name=groupname)) == 0:
groupname = '{0}_{1}'.format(role, loc.course_id)
return groupname
groupnames = []
groupnames.append('{0}_{1}'.format(role, location.course_id))
if isinstance(location, Location):
groupnames.append('{0}_{1}'.format(role, location.course))
elif isinstance(location, CourseLocator):
groupnames.append('{0}_{1}'.format(role, location.as_old_location_course_id))
for groupname in groupnames:
if Group.objects.filter(name=groupname).exists():
return groupname
return groupnames[0]
def get_users_in_course_group_by_role(location, role):
......
......@@ -3,6 +3,7 @@ from auth.authz import is_user_in_course_group_role
from django.core.exceptions import PermissionDenied
from ..utils import get_course_location_for_item
from xmodule.modulestore import Location
from xmodule.modulestore.locator import CourseLocator
def get_location_and_verify_access(request, org, course, name):
......@@ -29,13 +30,14 @@ def has_access(user, location, role=STAFF_ROLE_NAME):
will not be in both INSTRUCTOR and STAFF groups, so we have to cascade our
queries here as INSTRUCTOR has all the rights that STAFF do
'''
course_location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, course_location, role)
if not isinstance(location, CourseLocator):
location = get_course_location_for_item(location)
_has_access = is_user_in_course_group_role(user, location, role)
# if we're not in STAFF, perhaps we're in INSTRUCTOR groups
if not _has_access and role == STAFF_ROLE_NAME:
_has_access = is_user_in_course_group_role(
user,
course_location,
location,
INSTRUCTOR_ROLE_NAME
)
return _has_access
......@@ -12,11 +12,11 @@ from django.conf import settings
from django.views.decorators.http import require_http_methods, require_POST
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.http import HttpResponseBadRequest
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from util.json_request import JsonResponse
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore.inheritance import own_metadata
from xmodule.contentstore.content import StaticContent
......@@ -48,7 +48,8 @@ from django_comment_common.utils import seed_permissions_roles
from student.models import CourseEnrollment
from xmodule.html_module import AboutDescriptor
__all__ = ['course_index', 'create_new_course', 'course_info',
from xmodule.modulestore.locator import BlockUsageLocator
__all__ = ['create_new_course', 'course_info', 'course_handler',
'course_info_updates', 'get_course_settings',
'course_config_graders_page',
'course_config_advanced_page',
......@@ -59,24 +60,83 @@ __all__ = ['course_index', 'create_new_course', 'course_info',
@login_required
def course_handler(request, course_url):
"""
The restful handler for course specific requests.
It provides the course tree with the necessary information for identifying and labeling the parts. The root
will typically be a 'course' object but may not be especially as we support modules.
GET
html: return html page overview for the given course
json: return json representing the course branch's index entry as well as dag w/ all of the children
replaced w/ json docs where each doc has {'_id': , 'display_name': , 'children': }
POST
json: create (or update?) this course or branch in this course for this user, return resulting json
descriptor (same as in GET course/...). Leaving off /branch/draft would imply create the course w/ default
branches. Cannot change the structure contents ('_id', 'display_name', 'children') but can change the
index entry.
PUT
json: update this course (index entry not xblock) such as repointing head, changing display name, org,
course_id, prettyid. Return same json as above.
DELETE
json: delete this branch from this course (leaving off /branch/draft would imply delete the course)
"""
if 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
if request.method == 'GET':
raise NotImplementedError('coming soon')
elif not has_access(request.user, BlockUsageLocator(course_url)):
raise PermissionDenied()
elif request.method == 'POST':
raise NotImplementedError()
elif request.method == 'PUT':
raise NotImplementedError()
elif request.method == 'DELETE':
raise NotImplementedError()
else:
return HttpResponseBadRequest()
elif request.method == 'GET': # assume html
return course_index(request, course_url)
else:
return HttpResponseNotFound()
@login_required
@ensure_csrf_cookie
def course_index(request, org, course, name):
def old_course_index_shim(request, org, course, name):
"""
A shim for any unconverted uses of course_index
"""
old_location = Location(['i4x', org, course, 'course', name])
locator = loc_mapper().translate_location(old_location.course_id, old_location, False, True)
return course_index(request, locator)
@login_required
@ensure_csrf_cookie
def course_index(request, course_url):
"""
Display an editable course overview.
org, course, name: Attributes of the Location for the item to edit
"""
location = get_location_and_verify_access(request, org, course, name)
location = BlockUsageLocator(course_url)
# TODO: when converting to split backend, if location does not have a usage_id,
# we'll need to get the course's root block_id
if not has_access(request.user, location):
raise PermissionDenied()
lms_link = get_lms_link_for_item(location)
old_location = loc_mapper().translate_locator_to_location(location)
lms_link = get_lms_link_for_item(old_location)
upload_asset_callback_url = reverse('upload_asset', kwargs={
'org': org,
'course': course,
'coursename': name
'org': location.as_old_location_org,
'course': location.as_old_location_course,
'coursename': location.as_old_location_run
})
course = modulestore().get_item(location, depth=3)
course = modulestore().get_item(old_location, depth=3)
sections = course.get_children()
return render_to_response('overview.html', {
......
......@@ -11,7 +11,7 @@ from django_future.csrf import ensure_csrf_cookie
from mitxmako.shortcuts import render_to_response
from django.core.context_processors import csrf
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import modulestore, loc_mapper
from xmodule.modulestore import Location
from xmodule.error_module import ErrorDescriptor
from contentstore.utils import get_lms_link_for_item
......@@ -46,13 +46,13 @@ def index(request):
courses = filter(course_filter, courses)
def format_course_for_view(course):
# published = false b/c studio manipulates draft versions not b/c the course isn't pub'd
course_url = loc_mapper().translate_location(
course.location.course_id, course.location, published=False, add_entry_if_missing=True
)
return (
course.display_name,
reverse("course_index", kwargs={
'org': course.location.org,
'course': course.location.course,
'name': course.location.name,
}),
reverse("contentstore.views.course_handler", kwargs={'course_url': course_url}),
get_lms_link_for_item(
course.location
),
......
......@@ -2,6 +2,7 @@
<%!
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from xmodule.modulestore.django import loc_mapper
%>
<div class="wrapper-header wrapper" id="view-top">
......@@ -12,10 +13,16 @@
<h1 class="branding"><a href="/"><img src="${static.url("img/logo-edx-studio.png")}" alt="edX Studio" /></a></h1>
% if context_course:
<% ctx_loc = context_course.location %>
<%
ctx_loc = context_course.location
index_url = reverse(
'contentstore.views.course_handler',
kwargs={'course_url': loc_mapper().translate_location(ctx_loc.course_id, ctx_loc, False, True)}
)
%>
<h2 class="info-course">
<span class="sr">${_("Current Course:")}</span>
<a class="course-link" href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">
<a class="course-link" href="${index_url}">
<span class="course-org">${context_course.display_org_with_default | h}</span><span class="course-number">${context_course.display_number_with_default | h}</span>
<span class="course-title" title="${context_course.display_name_with_default}">${context_course.display_name_with_default}</span>
</a>
......@@ -31,7 +38,7 @@
<div class="nav-sub">
<ul>
<li class="nav-item nav-course-courseware-outline">
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Outline")}</a>
<a href="${index_url}">${_("Outline")}</a>
</li>
<li class="nav-item nav-course-courseware-updates">
<a href="${reverse('course_info', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}">${_("Updates")}</a>
......
......@@ -9,7 +9,7 @@ startup.run()
from ratelimitbackend import admin
admin.autodiscover()
urlpatterns = ('', # nopep8
urlpatterns = patterns('', # nopep8
url(r'^$', 'contentstore.views.howitworks', name='homepage'),
url(r'^listing', 'contentstore.views.index', name='index'),
url(r'^request_course_creator$', 'contentstore.views.request_course_creator', name='request_course_creator'),
......@@ -25,8 +25,6 @@ urlpatterns = ('', # nopep8
url(r'^create_new_course', 'contentstore.views.create_new_course', name='create_new_course'),
url(r'^reorder_static_tabs', 'contentstore.views.reorder_static_tabs', name='reorder_static_tabs'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
'contentstore.views.course_index', name='course_index'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import/(?P<name>[^/]+)$',
'contentstore.views.import_course', name='import_course'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import_status/(?P<name>[^/]+)$',
......@@ -106,7 +104,8 @@ urlpatterns = ('', # nopep8
)
# User creation and updating views
urlpatterns += (
urlpatterns += patterns(
'',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)$', 'contentstore.views.get_checklists', name='checklists'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)/update(/)?(?P<checklist_index>.+)?.*$',
'contentstore.views.update_checklist', name='checklists_updates'),
......@@ -125,22 +124,37 @@ urlpatterns += (
url(r'^logout$', 'student.views.logout_user', name='logout'),
)
# restful api
urlpatterns += patterns(
'contentstore.views',
# index page, course outline page, and course structure json access
# replaces url(r'^listing', 'contentstore.views.index', name='index'),
# ? url(r'^create_new_course', 'contentstore.views.create_new_course', name='create_new_course')
# TODO remove shim and this pattern once import_export and test_contentstore no longer use
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
'course.old_course_index_shim', name='course_index'
),
url(r'^course$', 'index'),
url(r'^course/(?P<course_url>.*)$', 'course_handler'),
)
js_info_dict = {
'domain': 'djangojs',
'packages': ('cms',),
}
urlpatterns += (
urlpatterns += patterns('',
# Serve catalog of localized strings to be rendered by Javascript
url(r'^i18n.js$', 'django.views.i18n.javascript_catalog', js_info_dict),
)
if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
urlpatterns += (
urlpatterns += patterns('',
url(r'^status/', include('service_status.urls')),
)
urlpatterns += (url(r'^admin/', include(admin.site.urls)),)
urlpatterns += patterns('', url(r'^admin/', include(admin.site.urls)),)
# enable automatic login
if settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
......@@ -155,8 +169,6 @@ if settings.DEBUG:
except ImportError:
pass
urlpatterns = patterns(*urlpatterns)
# Custom error pages
#pylint: disable=C0103
handler404 = 'contentstore.views.render_404'
......
......@@ -56,7 +56,7 @@ class LocMapperStore(object):
"""
Add a new entry to map this course_location to the new style CourseLocator.course_id. If course_id is not
provided, it creates the default map of using org.course.name from the location (just like course_id) if
the location.cateogry = 'course'; otherwise, it uses org.course.
the location.category = 'course'; otherwise, it uses org.course.
You can create more than one mapping to the
same course_id target. In that case, the reverse translate will be arbitrary (no guarantee of which wins).
......
......@@ -14,6 +14,8 @@ from xmodule.modulestore.exceptions import InsufficientSpecificationError, OverS
from .parsers import parse_url, parse_course_id, parse_block_ref
from .parsers import BRANCH_PREFIX, BLOCK_PREFIX, URL_VERSION_PREFIX
import re
from xmodule.modulestore import Location
log = logging.getLogger(__name__)
......@@ -89,6 +91,59 @@ class Locator(object):
(property_name, current, new))
setattr(self, property_name, new)
@staticmethod
def to_locator_or_location(location):
"""
Convert the given locator like thing to the appropriate type of object, or, if already
that type, just return it. Returns an old Location, BlockUsageLocator,
or DefinitionLocator.
:param location: can be a Location, Locator, string, tuple, list, or dict.
"""
if isinstance(location, (Location, Locator)):
return location
if isinstance(location, basestring):
return Locator.parse_url(location)
if isinstance(location, (list, tuple)):
return Location(location)
if isinstance(location, dict) and 'name' in location:
return Location(location)
if isinstance(location, dict):
return BlockUsageLocator(**location)
raise ValueError(location)
URL_TAG_RE = re.compile(r'^(\w+)://')
@staticmethod
def parse_url(url):
"""
Parse the url into one of the Locator types (must have a tag indicating type)
Return the new instance. Supports i4x, cvx, edx, defx
:param url: the url to parse
"""
parsed = Locator.URL_TAG_RE.match(url)
if parsed is None:
raise ValueError(parsed)
parsed = parsed.group(1)
if parsed in ['i4x', 'c4x']:
return Location(url)
elif parsed == 'edx':
return BlockUsageLocator(url)
elif parsed == 'defx':
return DefinitionLocator(url)
return None
@classmethod
def as_object_id(cls, value):
"""
Attempts to cast value as a bson.objectid.ObjectId.
If cast fails, raises ValueError
"""
try:
return ObjectId(value)
except InvalidId:
raise ValueError('"%s" is not a valid version_guid' % value)
class CourseLocator(Locator):
"""
......@@ -208,18 +263,55 @@ class CourseLocator(Locator):
version_guid=self.version_guid,
branch=self.branch)
@classmethod
def as_object_id(cls, value):
OLD_COURSE_ID_RE = re.compile(r'^(?P<org>[^.]+)\.?(?P<old_course_id>.+)?\.(?P<run>[^.]+)$')
@property
def as_old_location_course_id(self):
"""
Attempts to cast value as a bson.objectid.ObjectId.
If cast fails, raises ValueError
The original Location type presented its course id as org/course/run. This function
assumes the course_id starts w/ org, has an arbitrarily long 'course' identifier, and then
ends w/ run all separated by periods.
If this object does not have a course_id, this function returns None.
"""
if isinstance(value, ObjectId):
return value
try:
return ObjectId(value)
except InvalidId:
raise ValueError('"%s" is not a valid version_guid' % value)
if self.course_id is None:
return None
parsed = self.OLD_COURSE_ID_RE.match(self.course_id)
# check whether there are 2 or > 2 'fields'
if parsed.group('old_course_id'):
return '/'.join(parsed.groups())
else:
return parsed.group('org') + '/' + parsed.group('run')
def _old_location_field_helper(self, field):
"""
Parse course_id to get the old location field named field out
"""
if self.course_id is None:
return None
parsed = self.OLD_COURSE_ID_RE.match(self.course_id)
return parsed.group(field)
@property
def as_old_location_org(self):
"""
Presume the first part of the course_id is the org and return it.
"""
return self._old_location_field_helper('org')
@property
def as_old_location_course(self):
"""
Presume the middle part, if any, of the course_id is the old location scheme's
course id and return it.
"""
return self._old_location_field_helper('old_course_id')
@property
def as_old_location_run(self):
"""
Presume the last part of the course_id is the old location scheme's run and return it.
"""
return self._old_location_field_helper('run')
def init_from_url(self, url):
"""
......@@ -230,7 +322,7 @@ class CourseLocator(Locator):
url = url.url()
if not isinstance(url, basestring):
raise TypeError('%s is not an instance of basestring' % url)
parse = parse_url(url)
parse = parse_url(url, tag_optional=True)
if not parse:
raise ValueError('Could not parse "%s" as a url' % url)
self._set_value(
......@@ -349,7 +441,7 @@ class BlockUsageLocator(CourseLocator):
"""
self._validate_args(url, version_guid, course_id)
if url:
self.init_block_ref_from_url(url)
self.init_block_ref_from_str(url)
if course_id:
self.init_block_ref_from_course_id(course_id)
if usage_id:
......@@ -401,11 +493,18 @@ class BlockUsageLocator(CourseLocator):
raise ValueError('Could not parse "%s" as a block_ref' % block_ref)
self.set_usage_id(parse['block'])
def init_block_ref_from_url(self, url):
if isinstance(url, Locator):
url = url.url()
parse = parse_url(url)
assert parse, 'Could not parse "%s" as a url' % url
def init_block_ref_from_str(self, value):
"""
Create a block locator from the given string which may be a url or just the repr (no tag)
"""
if hasattr(value, 'usage_id'):
self.init_block_ref(value.usage_id)
return
if not isinstance(value, basestring):
return None
parse = parse_url(value, tag_optional=True)
if parse is None:
raise ValueError('Could not parse "%s" as a url' % value)
self._set_value(parse, 'block', lambda(new_block): self.set_usage_id(new_block))
def init_block_ref_from_course_id(self, course_id):
......@@ -429,8 +528,13 @@ class DefinitionLocator(Locator):
Container for how to locate a description (the course-independent content).
"""
URL_RE = re.compile(r'^defx://' + URL_VERSION_PREFIX + '([^/]+)$', re.IGNORECASE)
def __init__(self, definition_id):
self.definition_id = definition_id
if isinstance(definition_id, basestring):
regex_match = self.URL_RE.match(definition_id)
if regex_match is not None:
definition_id = self.as_object_id(regex_match.group(1))
self.definition_id = self.as_object_id(definition_id)
def __unicode__(self):
'''
......@@ -442,9 +546,9 @@ class DefinitionLocator(Locator):
def url(self):
"""
Return a string containing the URL for this location.
url(self) returns something like this: 'edx://version/519665f6223ebd6980884f2b'
url(self) returns something like this: 'defx://version/519665f6223ebd6980884f2b'
"""
return 'edx://' + unicode(self)
return 'defx://' + unicode(self)
def version(self):
"""
......
......@@ -9,13 +9,14 @@ VERSION_PREFIX = "/version/"
# Prefix for version when it begins the URL (no course ID).
URL_VERSION_PREFIX = 'version/'
URL_RE = re.compile(r'^edx://(.+)$', re.IGNORECASE)
URL_RE = re.compile(r'^(edx://)?(.+)$', re.IGNORECASE)
def parse_url(string):
def parse_url(string, tag_optional=False):
"""
A url must begin with 'edx://' (case-insensitive match),
followed by either a version_guid or a course_id.
A url usually begins with 'edx://' (case-insensitive match),
followed by either a version_guid or a course_id. If tag_optional, then
the url does not have to start with the tag and edx will be assumed.
Examples:
'edx://version/0123FFFF'
......@@ -36,7 +37,9 @@ def parse_url(string):
match = URL_RE.match(string)
if not match:
return None
path = match.group(1)
if match.group(1) is None and not tag_optional:
return None
path = match.group(2)
if path.startswith(URL_VERSION_PREFIX):
return parse_guid(path[len(URL_VERSION_PREFIX):])
return parse_course_id(path)
......
......@@ -135,7 +135,7 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertEqual(
len(course.children), 3,
"children")
self.assertEqual(course.definition_locator.definition_id, "head12345_12")
self.assertEqual(str(course.definition_locator.definition_id), "ad00000000000000dddd0000")
# check dates and graders--forces loading of descriptor
self.assertEqual(course.edited_by, "testassist@edx.org")
self.assertEqual(str(course.previous_version), self.GUID_D1)
......@@ -195,7 +195,7 @@ class SplitModuleCourseTests(SplitModuleTest):
self.assertEqual(course.graceperiod, datetime.timedelta(hours=2))
self.assertIsNone(course.advertised_start)
self.assertEqual(len(course.children), 0)
self.assertEqual(course.definition_locator.definition_id, "head12345_11")
self.assertEqual(str(course.definition_locator.definition_id), "ad00000000000000dddd0001")
# check dates and graders--forces loading of descriptor
self.assertEqual(course.edited_by, "testassist@edx.org")
self.assertDictEqual(course.grade_cutoffs, {"Pass": 0.55})
......@@ -345,7 +345,7 @@ class SplitModuleItemTests(SplitModuleTest):
self.assertEqual(block.display_name, "The Ancient Greek Hero")
self.assertEqual(block.advertised_start, "Fall 2013")
self.assertEqual(len(block.children), 3)
self.assertEqual(block.definition_locator.definition_id, "head12345_12")
self.assertEqual(str(block.definition_locator.definition_id), "ad00000000000000dddd0000")
# check dates and graders--forces loading of descriptor
self.assertEqual(block.edited_by, "testassist@edx.org")
self.assertDictEqual(
......@@ -375,7 +375,7 @@ class SplitModuleItemTests(SplitModuleTest):
block = modulestore().get_item(locator)
self.assertEqual(block.location.course_id, "GreekHero")
self.assertEqual(block.category, 'chapter')
self.assertEqual(block.definition_locator.definition_id, "chapter12345_1")
self.assertEqual(str(block.definition_locator.definition_id), "cd00000000000000dddd0020")
self.assertEqual(block.display_name, "Hercules")
self.assertEqual(block.edited_by, "testassist@edx.org")
......@@ -562,13 +562,13 @@ class TestItemCrud(SplitModuleTest):
new_module = modulestore().create_item(
locator, category, 'user123',
fields={'display_name': 'new chapter'},
definition_locator=DefinitionLocator("chapter12345_2")
definition_locator=DefinitionLocator("cd00000000000000dddd0022")
)
# check that course version changed and course's previous is the other one
self.assertNotEqual(new_module.location.version_guid, premod_course.location.version_guid)
parent = modulestore().get_item(locator)
self.assertIn(new_module.location.usage_id, parent.children)
self.assertEqual(new_module.definition_locator.definition_id, "chapter12345_2")
self.assertEqual(str(new_module.definition_locator.definition_id), "cd00000000000000dddd0022")
def test_unique_naming(self):
"""
......@@ -588,7 +588,7 @@ class TestItemCrud(SplitModuleTest):
another_module = modulestore().create_item(
locator, category, 'anotheruser',
fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=DefinitionLocator("problem12345_3_1"),
definition_locator=DefinitionLocator("0d00000040000000dddd0031"),
)
# check that course version changed and course's previous is the other one
parent = modulestore().get_item(locator)
......@@ -605,7 +605,7 @@ class TestItemCrud(SplitModuleTest):
self.assertLessEqual(new_history['edited_on'], datetime.datetime.now(UTC))
self.assertGreaterEqual(new_history['edited_on'], premod_time)
another_history = modulestore().get_definition_history_info(another_module.definition_locator)
self.assertEqual(another_history['previous_version'], 'problem12345_3_1')
self.assertEqual(str(another_history['previous_version']), '0d00000040000000dddd0031')
def test_create_continue_version(self):
"""
......@@ -789,7 +789,7 @@ class TestItemCrud(SplitModuleTest):
modulestore().create_item(
locator, category, 'test_update_manifold',
fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=DefinitionLocator("problem12345_3_1"),
definition_locator=DefinitionLocator("0d00000040000000dddd0031"),
)
# pylint: disable=W0212
modulestore()._clear_cache()
......
[
{
"_id":"head12345_12",
"_id": { "$oid" : "ad00000000000000dddd0000"},
"category":"course",
"fields":{
"textbooks":[
......@@ -46,12 +46,12 @@
"edit_info": {
"edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364481713238},
"previous_version":"head12345_11",
"original_version":"head12345_10"
"previous_version":{ "$oid" : "ad00000000000000dddd0001"},
"original_version":{ "$oid" : "ad00000000000000dddd0010"}
}
},
{
"_id":"head12345_11",
"_id":{ "$oid" : "ad00000000000000dddd0001"},
"category":"course",
"fields":{
"textbooks":[
......@@ -97,12 +97,12 @@
"edit_info": {
"edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364481713238},
"previous_version":"head12345_10",
"original_version":"head12345_10"
"previous_version":{ "$oid" : "ad00000000000000dddd0010"},
"original_version":{ "$oid" : "ad00000000000000dddd0010"}
}
},
{
"_id":"head12345_10",
"_id":{ "$oid" : "ad00000000000000dddd0010"},
"category":"course",
"fields":{
"textbooks":[
......@@ -149,11 +149,11 @@
"edited_by":"test@edx.org",
"edited_on":{"$date": 1364473713238},
"previous_version":null,
"original_version":"head12345_10"
"original_version":{ "$oid" : "ad00000000000000dddd0010"}
}
},
{
"_id":"head23456_1",
"_id":{ "$oid" : "ad00000000000000dddd0020"},
"category":"course",
"fields":{
"textbooks":[
......@@ -199,12 +199,12 @@
"edit_info": {
"edited_by":"test@edx.org",
"edited_on":{"$date": 1364481313238},
"previous_version":"head23456_0",
"original_version":"head23456_0"
"previous_version":{ "$oid" : "2d00000000000000dddd0020"},
"original_version":{ "$oid" : "2d00000000000000dddd0020"}
}
},
{
"_id":"head23456_0",
"_id":{ "$oid" : "2d00000000000000dddd0020"},
"category":"course",
"fields":{
"textbooks":[
......@@ -251,11 +251,11 @@
"edited_by":"test@edx.org",
"edited_on":{"$date" : 1364481313238},
"previous_version":null,
"original_version":"head23456_0"
"original_version":{ "$oid" : "2d00000000000000dddd0020"}
}
},
{
"_id":"head345679_1",
"_id":{ "$oid" : "3d00000000000000dddd0020"},
"category":"course",
"fields":{
"textbooks":[
......@@ -295,62 +295,62 @@
"edited_by":"test@edx.org",
"edited_on":{"$date" : 1364481313238},
"previous_version":null,
"original_version":"head23456_0"
"original_version":{ "$oid" : "2d00000000000000dddd0020"}
}
},
{
"_id":"chapter12345_1",
"_id":{ "$oid" : "cd00000000000000dddd0020"},
"category":"chapter",
"fields":{},
"edit_info": {
"edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238},
"previous_version":null,
"original_version":"chapter12345_1"
"original_version":{ "$oid" : "cd00000000000000dddd0020"}
}
},
{
"_id":"chapter12345_2",
"_id":{ "$oid" : "cd00000000000000dddd0022"},
"category":"chapter",
"fields":{},
"edit_info": {
"edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238},
"previous_version":null,
"original_version":"chapter12345_2"
"original_version":{ "$oid" : "cd00000000000000dddd0022"}
}
},
{
"_id":"chapter12345_3",
"_id":{ "$oid" : "cd00000000000000dddd0032"},
"category":"chapter",
"fields":{},
"edit_info": {
"edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238},
"previous_version":null,
"original_version":"chapter12345_3"
"original_version":{ "$oid" : "cd00000000000000dddd0032"}
}
},
{
"_id":"problem12345_3_1",
"_id":{ "$oid" : "0d00000040000000dddd0031"},
"category":"problem",
"fields": {"data": ""},
"edit_info": {
"edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238},
"previous_version":null,
"original_version":"problem12345_3_1"
"original_version":{ "$oid" : "0d00000040000000dddd0031"}
}
},
{
"_id":"problem12345_3_2",
"_id":{ "$oid" : "0d00000040000000dddd0032"},
"category":"problem",
"fields": {"data": ""},
"edit_info": {
"edited_by":"testassist@edx.org",
"edited_on":{"$date" : 1364483713238},
"previous_version":null,
"original_version":"problem12345_3_2"
"original_version":{ "$oid" : "0d00000040000000dddd0032"}
}
}
]
\ No newline at end of file
......@@ -11,7 +11,7 @@
"blocks":{
"head12345":{
"category":"course",
"definition":"head12345_12",
"definition":{ "$oid" : "ad00000000000000dddd0000"},
"fields":{
"children":[
"chapter1",
......@@ -65,7 +65,7 @@
},
"chapter1":{
"category":"chapter",
"definition":"chapter12345_1",
"definition":{ "$oid" : "cd00000000000000dddd0020"},
"fields":{
"children":[
......@@ -83,7 +83,7 @@
},
"chapter2":{
"category":"chapter",
"definition":"chapter12345_2",
"definition":{ "$oid" : "cd00000000000000dddd0022"},
"fields":{
"children":[
......@@ -101,7 +101,7 @@
},
"chapter3":{
"category":"chapter",
"definition":"chapter12345_3",
"definition":{ "$oid" : "cd00000000000000dddd0032"},
"fields":{
"children":[
"problem1",
......@@ -120,7 +120,7 @@
},
"problem1":{
"category":"problem",
"definition":"problem12345_3_1",
"definition":{ "$oid" : "0d00000040000000dddd0031"},
"fields":{
"children":[
......@@ -139,7 +139,7 @@
},
"problem3_2":{
"category":"problem",
"definition":"problem12345_3_2",
"definition":{ "$oid" : "0d00000040000000dddd0032"},
"fields":{
"children":[
......@@ -169,7 +169,7 @@
"blocks":{
"head12345":{
"category":"course",
"definition":"head12345_11",
"definition":{ "$oid" : "ad00000000000000dddd0001"},
"fields":{
"children":[
......@@ -233,7 +233,7 @@
"blocks":{
"head12345":{
"category":"course",
"definition":"head12345_10",
"definition":{ "$oid" : "ad00000000000000dddd0010"},
"fields":{
"children":[
......@@ -287,7 +287,7 @@
"blocks":{
"head23456":{
"category":"course",
"definition":"head23456_1",
"definition":{ "$oid" : "ad00000000000000dddd0020"},
"fields":{
"children":[
......@@ -342,7 +342,7 @@
"blocks":{
"head23456":{
"category":"course",
"definition":"head23456_0",
"definition":{ "$oid" : "2d00000000000000dddd0020"},
"fields":{
"children":[
......@@ -396,7 +396,7 @@
"blocks":{
"head23456":{
"category":"course",
"definition":"head23456_1",
"definition":{ "$oid" : "ad00000000000000dddd0020"},
"fields":{
"children":[
......@@ -450,7 +450,7 @@
"blocks":{
"head345679":{
"category":"course",
"definition":"head345679_1",
"definition":{ "$oid" : "3d00000000000000dddd0020"},
"fields":{
"children":[
......
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