Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
a6ad531d
Commit
a6ad531d
authored
Oct 17, 2013
by
Don Mitchell
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1287 from edx/dhm/restful_crud
Restful api prototype
parents
17d8bd21
d45beaeb
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
434 additions
and
130 deletions
+434
-130
cms/djangoapps/auth/authz.py
+19
-10
cms/djangoapps/contentstore/tests/test_course_index.py
+43
-0
cms/djangoapps/contentstore/tests/test_crud.py
+4
-1
cms/djangoapps/contentstore/tests/utils.py
+18
-0
cms/djangoapps/contentstore/views/access.py
+5
-3
cms/djangoapps/contentstore/views/course.py
+70
-10
cms/djangoapps/contentstore/views/user.py
+6
-6
cms/pydev_manage.py
+0
-11
cms/templates/widgets/header.html
+10
-3
cms/urls.py
+21
-9
common/lib/xmodule/xmodule/modulestore/django.py
+3
-0
common/lib/xmodule/xmodule/modulestore/loc_mapper_store.py
+1
-1
common/lib/xmodule/xmodule/modulestore/locator.py
+124
-20
common/lib/xmodule/xmodule/modulestore/parsers.py
+8
-5
common/lib/xmodule/xmodule/modulestore/tests/test_locators.py
+55
-5
common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
+9
-9
common/test/data/splitmongo_json/definitions.json
+26
-25
common/test/data/splitmongo_json/structures.json
+12
-12
No files found.
cms/djangoapps/auth/authz.py
View file @
a6ad531d
#=======================================================================================================================
#
# 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.contrib.auth.models
import
User
,
Group
from
django.core.exceptions
import
PermissionDenied
from
django.core.exceptions
import
PermissionDenied
from
django.conf
import
settings
from
django.conf
import
settings
from
xmodule.modulestore
import
Location
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
# define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes
INSTRUCTOR_ROLE_NAME
=
'instructor'
INSTRUCTOR_ROLE_NAME
=
'instructor'
...
@@ -22,16 +25,22 @@ COURSE_CREATOR_GROUP_NAME = "course_creator_group"
...
@@ -22,16 +25,22 @@ COURSE_CREATOR_GROUP_NAME = "course_creator_group"
def
get_course_groupname_for_role
(
location
,
role
):
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>
# 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
# if it exists, then use that one, otherwise use a <role>_<course_id> which contains
# more information
# more information
groupname
=
'{0}_{1}'
.
format
(
role
,
loc
.
course
)
groupnames
=
[]
groupnames
.
append
(
'{0}_{1}'
.
format
(
role
,
location
.
course_id
))
if
len
(
Group
.
objects
.
filter
(
name
=
groupname
))
==
0
:
if
isinstance
(
location
,
Location
):
groupname
=
'{0}_{1}'
.
format
(
role
,
loc
.
course_id
)
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
groupname
return
groupnames
[
0
]
def
get_users_in_course_group_by_role
(
location
,
role
):
def
get_users_in_course_group_by_role
(
location
,
role
):
...
...
cms/djangoapps/contentstore/tests/test_course_index.py
0 → 100644
View file @
a6ad531d
"""
Unit tests for getting the list of courses and the course outline.
"""
from
django.core.urlresolvers
import
reverse
import
lxml
from
contentstore.tests.utils
import
CourseTestCase
from
xmodule.modulestore.django
import
loc_mapper
from
django.core.exceptions
import
PermissionDenied
class
TestCourseIndex
(
CourseTestCase
):
"""
Unit tests for getting the list of courses and the course outline.
"""
def
test_index
(
self
):
"""
Test getting the list of courses and then pulling up their outlines
"""
index_url
=
reverse
(
'contentstore.views.index'
)
index_response
=
self
.
client
.
get
(
index_url
,
{},
HTTP_ACCEPT
=
'text/html'
)
parsed_html
=
lxml
.
html
.
fromstring
(
index_response
.
content
)
course_link_eles
=
parsed_html
.
find_class
(
'course-link'
)
for
link
in
course_link_eles
:
self
.
assertRegexpMatches
(
link
.
get
(
"href"
),
r'course/\w+\.\w+\.\w+.*/branch/\w+/block/.*'
)
# now test that url
outline_response
=
self
.
client
.
get
(
link
.
get
(
"href"
),
{},
HTTP_ACCEPT
=
'text/html'
)
# ensure it has the expected 2 self referential links
outline_parsed
=
lxml
.
html
.
fromstring
(
outline_response
.
content
)
outline_link
=
outline_parsed
.
find_class
(
'course-link'
)[
0
]
self
.
assertEqual
(
outline_link
.
get
(
"href"
),
link
.
get
(
"href"
))
course_menu_link
=
outline_parsed
.
find_class
(
'nav-course-courseware-outline'
)[
0
]
self
.
assertEqual
(
course_menu_link
.
find
(
"a"
)
.
get
(
"href"
),
link
.
get
(
"href"
))
def
test_negative_conditions
(
self
):
"""
Test the error conditions for the access
"""
locator
=
loc_mapper
()
.
translate_location
(
self
.
course
.
location
.
course_id
,
self
.
course
.
location
,
False
,
True
)
outline_url
=
reverse
(
'contentstore.views.course_handler'
,
kwargs
=
{
'course_url'
:
unicode
(
locator
)})
# register a non-staff member and try to delete the course branch
non_staff_client
=
self
.
createNonStaffAuthedUserClient
()
response
=
non_staff_client
.
delete
(
outline_url
,
{},
HTTP_ACCEPT
=
'application/json'
)
self
.
assertEqual
(
response
.
status_code
,
403
)
cms/djangoapps/contentstore/tests/test_crud.py
View file @
a6ad531d
...
@@ -2,7 +2,7 @@ import unittest
...
@@ -2,7 +2,7 @@ import unittest
from
xmodule
import
templates
from
xmodule
import
templates
from
xmodule.modulestore.tests
import
persistent_factories
from
xmodule.modulestore.tests
import
persistent_factories
from
xmodule.course_module
import
CourseDescriptor
from
xmodule.course_module
import
CourseDescriptor
from
xmodule.modulestore.django
import
modulestore
,
loc_mapper
from
xmodule.modulestore.django
import
modulestore
,
loc_mapper
,
clear_existing_modulestores
from
xmodule.seq_module
import
SequenceDescriptor
from
xmodule.seq_module
import
SequenceDescriptor
from
xmodule.capa_module
import
CapaDescriptor
from
xmodule.capa_module
import
CapaDescriptor
from
xmodule.modulestore.locator
import
CourseLocator
,
BlockUsageLocator
from
xmodule.modulestore.locator
import
CourseLocator
,
BlockUsageLocator
...
@@ -17,6 +17,9 @@ class TemplateTests(unittest.TestCase):
...
@@ -17,6 +17,9 @@ class TemplateTests(unittest.TestCase):
Test finding and using the templates (boilerplates) for xblocks.
Test finding and using the templates (boilerplates) for xblocks.
"""
"""
def
setUp
(
self
):
clear_existing_modulestores
()
def
test_get_templates
(
self
):
def
test_get_templates
(
self
):
found
=
templates
.
all_templates
()
found
=
templates
.
all_templates
()
self
.
assertIsNotNone
(
found
.
get
(
'course'
))
self
.
assertIsNotNone
(
found
.
get
(
'course'
))
...
...
cms/djangoapps/contentstore/tests/utils.py
View file @
a6ad531d
...
@@ -61,3 +61,21 @@ class CourseTestCase(ModuleStoreTestCase):
...
@@ -61,3 +61,21 @@ class CourseTestCase(ModuleStoreTestCase):
number
=
'999'
,
number
=
'999'
,
display_name
=
'Robot Super Course'
,
display_name
=
'Robot Super Course'
,
)
)
def
createNonStaffAuthedUserClient
(
self
):
"""
Create a non-staff user, log them in, and return the client to use for testing.
"""
uname
=
'teststudent'
password
=
'foo'
nonstaff
=
User
.
objects
.
create_user
(
uname
,
'test+student@edx.org'
,
password
)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
nonstaff
.
is_active
=
True
nonstaff
.
is_staff
=
False
nonstaff
.
save
()
client
=
Client
()
client
.
login
(
username
=
uname
,
password
=
password
)
return
client
cms/djangoapps/contentstore/views/access.py
View file @
a6ad531d
...
@@ -3,6 +3,7 @@ from auth.authz import is_user_in_course_group_role
...
@@ -3,6 +3,7 @@ from auth.authz import is_user_in_course_group_role
from
django.core.exceptions
import
PermissionDenied
from
django.core.exceptions
import
PermissionDenied
from
..utils
import
get_course_location_for_item
from
..utils
import
get_course_location_for_item
from
xmodule.modulestore
import
Location
from
xmodule.modulestore
import
Location
from
xmodule.modulestore.locator
import
CourseLocator
def
get_location_and_verify_access
(
request
,
org
,
course
,
name
):
def
get_location_and_verify_access
(
request
,
org
,
course
,
name
):
...
@@ -29,13 +30,14 @@ def has_access(user, location, role=STAFF_ROLE_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
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
queries here as INSTRUCTOR has all the rights that STAFF do
'''
'''
course_location
=
get_course_location_for_item
(
location
)
if
not
isinstance
(
location
,
CourseLocator
):
_has_access
=
is_user_in_course_group_role
(
user
,
course_location
,
role
)
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 we're not in STAFF, perhaps we're in INSTRUCTOR groups
if
not
_has_access
and
role
==
STAFF_ROLE_NAME
:
if
not
_has_access
and
role
==
STAFF_ROLE_NAME
:
_has_access
=
is_user_in_course_group_role
(
_has_access
=
is_user_in_course_group_role
(
user
,
user
,
course_
location
,
location
,
INSTRUCTOR_ROLE_NAME
INSTRUCTOR_ROLE_NAME
)
)
return
_has_access
return
_has_access
cms/djangoapps/contentstore/views/course.py
View file @
a6ad531d
...
@@ -12,11 +12,11 @@ from django.conf import settings
...
@@ -12,11 +12,11 @@ from django.conf import settings
from
django.views.decorators.http
import
require_http_methods
,
require_POST
from
django.views.decorators.http
import
require_http_methods
,
require_POST
from
django.core.exceptions
import
PermissionDenied
from
django.core.exceptions
import
PermissionDenied
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.http
import
HttpResponseBadRequest
from
django.http
import
HttpResponseBadRequest
,
HttpResponseNotFound
from
util.json_request
import
JsonResponse
from
util.json_request
import
JsonResponse
from
mitxmako.shortcuts
import
render_to_response
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.modulestore.inheritance
import
own_metadata
from
xmodule.contentstore.content
import
StaticContent
from
xmodule.contentstore.content
import
StaticContent
...
@@ -48,7 +48,8 @@ from django_comment_common.utils import seed_permissions_roles
...
@@ -48,7 +48,8 @@ from django_comment_common.utils import seed_permissions_roles
from
student.models
import
CourseEnrollment
from
student.models
import
CourseEnrollment
from
xmodule.html_module
import
AboutDescriptor
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_info_updates'
,
'get_course_settings'
,
'course_config_graders_page'
,
'course_config_graders_page'
,
'course_config_advanced_page'
,
'course_config_advanced_page'
,
...
@@ -59,24 +60,83 @@ __all__ = ['course_index', 'create_new_course', 'course_info',
...
@@ -59,24 +60,83 @@ __all__ = ['course_index', 'create_new_course', 'course_info',
@login_required
@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
@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.
Display an editable course overview.
org, course, name: Attributes of the Location for the item to edit
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
()
old_location
=
loc_mapper
()
.
translate_locator_to_location
(
location
)
lms_link
=
get_lms_link_for_item
(
location
)
lms_link
=
get_lms_link_for_item
(
old_
location
)
upload_asset_callback_url
=
reverse
(
'upload_asset'
,
kwargs
=
{
upload_asset_callback_url
=
reverse
(
'upload_asset'
,
kwargs
=
{
'org'
:
org
,
'org'
:
location
.
as_old_location_
org
,
'course'
:
course
,
'course'
:
location
.
as_old_location_
course
,
'coursename'
:
name
'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
()
sections
=
course
.
get_children
()
return
render_to_response
(
'overview.html'
,
{
return
render_to_response
(
'overview.html'
,
{
...
...
cms/djangoapps/contentstore/views/user.py
View file @
a6ad531d
...
@@ -11,7 +11,7 @@ from django_future.csrf import ensure_csrf_cookie
...
@@ -11,7 +11,7 @@ from django_future.csrf import ensure_csrf_cookie
from
mitxmako.shortcuts
import
render_to_response
from
mitxmako.shortcuts
import
render_to_response
from
django.core.context_processors
import
csrf
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.modulestore
import
Location
from
xmodule.error_module
import
ErrorDescriptor
from
xmodule.error_module
import
ErrorDescriptor
from
contentstore.utils
import
get_lms_link_for_item
from
contentstore.utils
import
get_lms_link_for_item
...
@@ -46,13 +46,13 @@ def index(request):
...
@@ -46,13 +46,13 @@ def index(request):
courses
=
filter
(
course_filter
,
courses
)
courses
=
filter
(
course_filter
,
courses
)
def
format_course_for_view
(
course
):
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
(
return
(
course
.
display_name
,
course
.
display_name
,
reverse
(
"course_index"
,
kwargs
=
{
reverse
(
"contentstore.views.course_handler"
,
kwargs
=
{
'course_url'
:
course_url
}),
'org'
:
course
.
location
.
org
,
'course'
:
course
.
location
.
course
,
'name'
:
course
.
location
.
name
,
}),
get_lms_link_for_item
(
get_lms_link_for_item
(
course
.
location
course
.
location
),
),
...
...
cms/pydev_manage.py
deleted
100644 → 0
View file @
17d8bd21
'''
Used for pydev eclipse. Should be innocuous for everyone else.
Created on May 8, 2013
@author: dmitchell
'''
#!/home/<username>/mitx_all/python/bin/python
from
django.core
import
management
if
__name__
==
'__main__'
:
management
.
execute_from_command_line
()
cms/templates/widgets/header.html
View file @
a6ad531d
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
<
%!
<
%!
from
django
.
core
.
urlresolvers
import
reverse
from
django
.
core
.
urlresolvers
import
reverse
from
django
.
utils
.
translation
import
ugettext
as
_
from
django
.
utils
.
translation
import
ugettext
as
_
from
xmodule
.
modulestore
.
django
import
loc_mapper
%
>
%
>
<div
class=
"wrapper-header wrapper"
id=
"view-top"
>
<div
class=
"wrapper-header wrapper"
id=
"view-top"
>
...
@@ -12,10 +13,16 @@
...
@@ -12,10 +13,16 @@
<h1
class=
"branding"
><a
href=
"/"
><img
src=
"${static.url("
img
/
logo-edx-studio
.
png
")}"
alt=
"edX Studio"
/></a></h1>
<h1
class=
"branding"
><a
href=
"/"
><img
src=
"${static.url("
img
/
logo-edx-studio
.
png
")}"
alt=
"edX Studio"
/></a></h1>
% if context_course:
% 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"
>
<h2
class=
"info-course"
>
<span
class=
"sr"
>
${_("Current Course:")}
</span>
<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-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>
<span
class=
"course-title"
title=
"${context_course.display_name_with_default}"
>
${context_course.display_name_with_default}
</span>
</a>
</a>
...
@@ -31,7 +38,7 @@
...
@@ -31,7 +38,7 @@
<div
class=
"nav-sub"
>
<div
class=
"nav-sub"
>
<ul>
<ul>
<li
class=
"nav-item nav-course-courseware-outline"
>
<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>
<li
class=
"nav-item nav-course-courseware-updates"
>
<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>
<a
href=
"${reverse('course_info', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}"
>
${_("Updates")}
</a>
...
...
cms/urls.py
View file @
a6ad531d
...
@@ -9,7 +9,7 @@ startup.run()
...
@@ -9,7 +9,7 @@ startup.run()
from
ratelimitbackend
import
admin
from
ratelimitbackend
import
admin
admin
.
autodiscover
()
admin
.
autodiscover
()
urlpatterns
=
(
''
,
# nopep8
urlpatterns
=
patterns
(
''
,
# nopep8
url
(
r'^$'
,
'contentstore.views.howitworks'
,
name
=
'homepage'
),
url
(
r'^$'
,
'contentstore.views.howitworks'
,
name
=
'homepage'
),
url
(
r'^listing'
,
'contentstore.views.index'
,
name
=
'index'
),
url
(
r'^listing'
,
'contentstore.views.index'
,
name
=
'index'
),
url
(
r'^request_course_creator$'
,
'contentstore.views.request_course_creator'
,
name
=
'request_course_creator'
),
url
(
r'^request_course_creator$'
,
'contentstore.views.request_course_creator'
,
name
=
'request_course_creator'
),
...
@@ -25,8 +25,6 @@ urlpatterns = ('', # nopep8
...
@@ -25,8 +25,6 @@ urlpatterns = ('', # nopep8
url
(
r'^create_new_course'
,
'contentstore.views.create_new_course'
,
name
=
'create_new_course'
),
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'^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>[^/]+)$'
,
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import/(?P<name>[^/]+)$'
,
'contentstore.views.import_course'
,
name
=
'import_course'
),
'contentstore.views.import_course'
,
name
=
'import_course'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import_status/(?P<name>[^/]+)$'
,
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/import_status/(?P<name>[^/]+)$'
,
...
@@ -106,7 +104,8 @@ urlpatterns = ('', # nopep8
...
@@ -106,7 +104,8 @@ urlpatterns = ('', # nopep8
)
)
# User creation and updating views
# 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>[^/]+)$'
,
'contentstore.views.get_checklists'
,
name
=
'checklists'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)/update(/)?(?P<checklist_index>.+)?.*$'
,
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/checklists/(?P<name>[^/]+)/update(/)?(?P<checklist_index>.+)?.*$'
,
'contentstore.views.update_checklist'
,
name
=
'checklists_updates'
),
'contentstore.views.update_checklist'
,
name
=
'checklists_updates'
),
...
@@ -125,22 +124,37 @@ urlpatterns += (
...
@@ -125,22 +124,37 @@ urlpatterns += (
url
(
r'^logout$'
,
'student.views.logout_user'
,
name
=
'logout'
),
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
=
{
js_info_dict
=
{
'domain'
:
'djangojs'
,
'domain'
:
'djangojs'
,
'packages'
:
(
'cms'
,),
'packages'
:
(
'cms'
,),
}
}
urlpatterns
+=
(
urlpatterns
+=
patterns
(
''
,
# Serve catalog of localized strings to be rendered by Javascript
# Serve catalog of localized strings to be rendered by Javascript
url
(
r'^i18n.js$'
,
'django.views.i18n.javascript_catalog'
,
js_info_dict
),
url
(
r'^i18n.js$'
,
'django.views.i18n.javascript_catalog'
,
js_info_dict
),
)
)
if
settings
.
MITX_FEATURES
.
get
(
'ENABLE_SERVICE_STATUS'
):
if
settings
.
MITX_FEATURES
.
get
(
'ENABLE_SERVICE_STATUS'
):
urlpatterns
+=
(
urlpatterns
+=
patterns
(
''
,
url
(
r'^status/'
,
include
(
'service_status.urls'
)),
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
# enable automatic login
if
settings
.
MITX_FEATURES
.
get
(
'AUTOMATIC_AUTH_FOR_TESTING'
):
if
settings
.
MITX_FEATURES
.
get
(
'AUTOMATIC_AUTH_FOR_TESTING'
):
...
@@ -155,8 +169,6 @@ if settings.DEBUG:
...
@@ -155,8 +169,6 @@ if settings.DEBUG:
except
ImportError
:
except
ImportError
:
pass
pass
urlpatterns
=
patterns
(
*
urlpatterns
)
# Custom error pages
# Custom error pages
#pylint: disable=C0103
#pylint: disable=C0103
handler404
=
'contentstore.views.render_404'
handler404
=
'contentstore.views.render_404'
...
...
common/lib/xmodule/xmodule/modulestore/django.py
View file @
a6ad531d
...
@@ -141,6 +141,9 @@ def clear_existing_modulestores():
...
@@ -141,6 +141,9 @@ def clear_existing_modulestores():
This is useful for flushing state between unit tests.
This is useful for flushing state between unit tests.
"""
"""
_MODULESTORES
.
clear
()
_MODULESTORES
.
clear
()
# pylint: disable=W0603
global
_loc_singleton
_loc_singleton
=
None
def
editable_modulestore
(
name
=
'default'
):
def
editable_modulestore
(
name
=
'default'
):
...
...
common/lib/xmodule/xmodule/modulestore/loc_mapper_store.py
View file @
a6ad531d
...
@@ -56,7 +56,7 @@ class LocMapperStore(object):
...
@@ -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
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
provided, it creates the default map of using org.course.name from the location (just like course_id) if
the location.cate
og
ry = 'course'; otherwise, it uses org.course.
the location.cate
go
ry = 'course'; otherwise, it uses org.course.
You can create more than one mapping to the
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).
same course_id target. In that case, the reverse translate will be arbitrary (no guarantee of which wins).
...
...
common/lib/xmodule/xmodule/modulestore/locator.py
View file @
a6ad531d
...
@@ -14,6 +14,8 @@ from xmodule.modulestore.exceptions import InsufficientSpecificationError, OverS
...
@@ -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
parse_url
,
parse_course_id
,
parse_block_ref
from
.parsers
import
BRANCH_PREFIX
,
BLOCK_PREFIX
,
URL_VERSION_PREFIX
from
.parsers
import
BRANCH_PREFIX
,
BLOCK_PREFIX
,
URL_VERSION_PREFIX
import
re
from
xmodule.modulestore
import
Location
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
...
@@ -89,6 +91,59 @@ class Locator(object):
...
@@ -89,6 +91,59 @@ class Locator(object):
(
property_name
,
current
,
new
))
(
property_name
,
current
,
new
))
setattr
(
self
,
property_name
,
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
):
class
CourseLocator
(
Locator
):
"""
"""
...
@@ -208,18 +263,55 @@ class CourseLocator(Locator):
...
@@ -208,18 +263,55 @@ class CourseLocator(Locator):
version_guid
=
self
.
version_guid
,
version_guid
=
self
.
version_guid
,
branch
=
self
.
branch
)
branch
=
self
.
branch
)
@classmethod
OLD_COURSE_ID_RE
=
re
.
compile
(
r'^(?P<org>[^.]+)\.?(?P<old_course_id>.+)?\.(?P<run>[^.]+)$'
)
def
as_object_id
(
cls
,
value
):
@property
def
as_old_location_course_id
(
self
):
"""
"""
Attempts to cast value as a bson.objectid.ObjectId.
The original Location type presented its course id as org/course/run. This function
If cast fails, raises ValueError
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
):
if
self
.
course_id
is
None
:
return
value
return
None
try
:
parsed
=
self
.
OLD_COURSE_ID_RE
.
match
(
self
.
course_id
)
return
ObjectId
(
value
)
# check whether there are 2 or > 2 'fields'
except
InvalidId
:
if
parsed
.
group
(
'old_course_id'
):
raise
ValueError
(
'"
%
s" is not a valid version_guid'
%
value
)
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
):
def
init_from_url
(
self
,
url
):
"""
"""
...
@@ -230,7 +322,7 @@ class CourseLocator(Locator):
...
@@ -230,7 +322,7 @@ class CourseLocator(Locator):
url
=
url
.
url
()
url
=
url
.
url
()
if
not
isinstance
(
url
,
basestring
):
if
not
isinstance
(
url
,
basestring
):
raise
TypeError
(
'
%
s is not an instance of basestring'
%
url
)
raise
TypeError
(
'
%
s is not an instance of basestring'
%
url
)
parse
=
parse_url
(
url
)
parse
=
parse_url
(
url
,
tag_optional
=
True
)
if
not
parse
:
if
not
parse
:
raise
ValueError
(
'Could not parse "
%
s" as a url'
%
url
)
raise
ValueError
(
'Could not parse "
%
s" as a url'
%
url
)
self
.
_set_value
(
self
.
_set_value
(
...
@@ -349,7 +441,7 @@ class BlockUsageLocator(CourseLocator):
...
@@ -349,7 +441,7 @@ class BlockUsageLocator(CourseLocator):
"""
"""
self
.
_validate_args
(
url
,
version_guid
,
course_id
)
self
.
_validate_args
(
url
,
version_guid
,
course_id
)
if
url
:
if
url
:
self
.
init_block_ref_from_
url
(
url
)
self
.
init_block_ref_from_
str
(
url
)
if
course_id
:
if
course_id
:
self
.
init_block_ref_from_course_id
(
course_id
)
self
.
init_block_ref_from_course_id
(
course_id
)
if
usage_id
:
if
usage_id
:
...
@@ -401,11 +493,18 @@ class BlockUsageLocator(CourseLocator):
...
@@ -401,11 +493,18 @@ class BlockUsageLocator(CourseLocator):
raise
ValueError
(
'Could not parse "
%
s" as a block_ref'
%
block_ref
)
raise
ValueError
(
'Could not parse "
%
s" as a block_ref'
%
block_ref
)
self
.
set_usage_id
(
parse
[
'block'
])
self
.
set_usage_id
(
parse
[
'block'
])
def
init_block_ref_from_url
(
self
,
url
):
def
init_block_ref_from_str
(
self
,
value
):
if
isinstance
(
url
,
Locator
):
"""
url
=
url
.
url
()
Create a block locator from the given string which may be a url or just the repr (no tag)
parse
=
parse_url
(
url
)
"""
assert
parse
,
'Could not parse "
%
s" as a url'
%
url
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
))
self
.
_set_value
(
parse
,
'block'
,
lambda
(
new_block
):
self
.
set_usage_id
(
new_block
))
def
init_block_ref_from_course_id
(
self
,
course_id
):
def
init_block_ref_from_course_id
(
self
,
course_id
):
...
@@ -429,8 +528,13 @@ class DefinitionLocator(Locator):
...
@@ -429,8 +528,13 @@ class DefinitionLocator(Locator):
Container for how to locate a description (the course-independent content).
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
):
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
):
def
__unicode__
(
self
):
'''
'''
...
@@ -442,9 +546,9 @@ class DefinitionLocator(Locator):
...
@@ -442,9 +546,9 @@ class DefinitionLocator(Locator):
def
url
(
self
):
def
url
(
self
):
"""
"""
Return a string containing the URL for this location.
Return a string containing the URL for this location.
url(self) returns something like this: '
ed
x://version/519665f6223ebd6980884f2b'
url(self) returns something like this: '
def
x://version/519665f6223ebd6980884f2b'
"""
"""
return
'
ed
x://'
+
unicode
(
self
)
return
'
def
x://'
+
unicode
(
self
)
def
version
(
self
):
def
version
(
self
):
"""
"""
...
...
common/lib/xmodule/xmodule/modulestore/parsers.py
View file @
a6ad531d
...
@@ -9,13 +9,14 @@ VERSION_PREFIX = "/version/"
...
@@ -9,13 +9,14 @@ VERSION_PREFIX = "/version/"
# Prefix for version when it begins the URL (no course ID).
# Prefix for version when it begins the URL (no course ID).
URL_VERSION_PREFIX
=
'version/'
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),
A url usually begins with 'edx://' (case-insensitive match),
followed by either a version_guid or a course_id.
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:
Examples:
'edx://version/0123FFFF'
'edx://version/0123FFFF'
...
@@ -36,7 +37,9 @@ def parse_url(string):
...
@@ -36,7 +37,9 @@ def parse_url(string):
match
=
URL_RE
.
match
(
string
)
match
=
URL_RE
.
match
(
string
)
if
not
match
:
if
not
match
:
return
None
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
):
if
path
.
startswith
(
URL_VERSION_PREFIX
):
return
parse_guid
(
path
[
len
(
URL_VERSION_PREFIX
):])
return
parse_guid
(
path
[
len
(
URL_VERSION_PREFIX
):])
return
parse_course_id
(
path
)
return
parse_course_id
(
path
)
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_locators.py
View file @
a6ad531d
...
@@ -7,6 +7,8 @@ from bson.objectid import ObjectId
...
@@ -7,6 +7,8 @@ from bson.objectid import ObjectId
from
xmodule.modulestore.locator
import
Locator
,
CourseLocator
,
BlockUsageLocator
,
DefinitionLocator
from
xmodule.modulestore.locator
import
Locator
,
CourseLocator
,
BlockUsageLocator
,
DefinitionLocator
from
xmodule.modulestore.parsers
import
BRANCH_PREFIX
,
BLOCK_PREFIX
,
VERSION_PREFIX
,
URL_VERSION_PREFIX
from
xmodule.modulestore.parsers
import
BRANCH_PREFIX
,
BLOCK_PREFIX
,
VERSION_PREFIX
,
URL_VERSION_PREFIX
from
xmodule.modulestore.exceptions
import
InsufficientSpecificationError
,
OverSpecificationError
from
xmodule.modulestore.exceptions
import
InsufficientSpecificationError
,
OverSpecificationError
from
xmodule.modulestore
import
Location
import
random
class
LocatorTest
(
TestCase
):
class
LocatorTest
(
TestCase
):
...
@@ -98,7 +100,6 @@ class LocatorTest(TestCase):
...
@@ -98,7 +100,6 @@ class LocatorTest(TestCase):
for
bad_url
in
(
'edx://'
,
for
bad_url
in
(
'edx://'
,
'edx:/mit.eecs'
,
'edx:/mit.eecs'
,
'http://mit.eecs'
,
'http://mit.eecs'
,
'mit.eecs'
,
'edx//mit.eecs'
):
'edx//mit.eecs'
):
self
.
assertRaises
(
ValueError
,
CourseLocator
,
url
=
bad_url
)
self
.
assertRaises
(
ValueError
,
CourseLocator
,
url
=
bad_url
)
...
@@ -253,13 +254,62 @@ class LocatorTest(TestCase):
...
@@ -253,13 +254,62 @@ class LocatorTest(TestCase):
testobj
=
BlockUsageLocator
(
course_id
=
testurn
)
testobj
=
BlockUsageLocator
(
course_id
=
testurn
)
self
.
assertEqual
(
'BlockUsageLocator("mit.eecs.6002x/branch/published/block/HW3")'
,
repr
(
testobj
))
self
.
assertEqual
(
'BlockUsageLocator("mit.eecs.6002x/branch/published/block/HW3")'
,
repr
(
testobj
))
def
test_old_location_helpers
(
self
):
"""
Test the functions intended to help with the conversion from old locations to locators
"""
location_tuple
=
(
'i4x'
,
'mit'
,
'eecs.6002x'
,
'course'
,
't3_2013'
)
location
=
Location
(
location_tuple
)
self
.
assertEqual
(
location
,
Locator
.
to_locator_or_location
(
location
))
self
.
assertEqual
(
location
,
Locator
.
to_locator_or_location
(
location_tuple
))
self
.
assertEqual
(
location
,
Locator
.
to_locator_or_location
(
list
(
location_tuple
)))
self
.
assertEqual
(
location
,
Locator
.
to_locator_or_location
(
location
.
dict
()))
locator
=
BlockUsageLocator
(
course_id
=
'foo.bar'
,
branch
=
'alpha'
,
usage_id
=
'deep'
)
self
.
assertEqual
(
locator
,
Locator
.
to_locator_or_location
(
locator
))
self
.
assertEqual
(
locator
.
as_course_locator
(),
Locator
.
to_locator_or_location
(
locator
.
as_course_locator
()))
self
.
assertEqual
(
location
,
Locator
.
to_locator_or_location
(
location
.
url
()))
self
.
assertEqual
(
locator
,
Locator
.
to_locator_or_location
(
locator
.
url
()))
self
.
assertEqual
(
locator
,
Locator
.
to_locator_or_location
(
locator
.
__dict__
))
asset_location
=
Location
([
'c4x'
,
'mit'
,
'eecs.6002x'
,
'asset'
,
'selfie.jpeg'
])
self
.
assertEqual
(
asset_location
,
Locator
.
to_locator_or_location
(
asset_location
))
self
.
assertEqual
(
asset_location
,
Locator
.
to_locator_or_location
(
asset_location
.
url
()))
def_location_url
=
"defx://version/"
+
'{:024x}'
.
format
(
random
.
randrange
(
16
**
24
))
self
.
assertEqual
(
DefinitionLocator
(
def_location_url
),
Locator
.
to_locator_or_location
(
def_location_url
))
with
self
.
assertRaises
(
ValueError
):
Locator
.
to_locator_or_location
(
22
)
with
self
.
assertRaises
(
ValueError
):
Locator
.
to_locator_or_location
(
"hello.world.not.a.url"
)
self
.
assertIsNone
(
Locator
.
parse_url
(
"unknown://foo.bar/baz"
))
def
test_as_old
(
self
):
"""
Test the as_old_location_xxx accessors
"""
locator
=
CourseLocator
(
course_id
=
'org.course.id.run'
,
branch
=
'mybranch'
)
self
.
assertEqual
(
'org'
,
locator
.
as_old_location_org
)
self
.
assertEqual
(
'course.id'
,
locator
.
as_old_location_course
)
self
.
assertEqual
(
'run'
,
locator
.
as_old_location_run
)
self
.
assertEqual
(
'org/course.id/run'
,
locator
.
as_old_location_course_id
)
locator
=
CourseLocator
(
course_id
=
'org.course'
,
branch
=
'mybranch'
)
self
.
assertEqual
(
'org'
,
locator
.
as_old_location_org
)
self
.
assertIsNone
(
locator
.
as_old_location_course
)
self
.
assertEqual
(
'course'
,
locator
.
as_old_location_run
)
self
.
assertEqual
(
'org/course'
,
locator
.
as_old_location_course_id
)
def
test_description_locator_url
(
self
):
def
test_description_locator_url
(
self
):
definition_locator
=
DefinitionLocator
(
"chapter12345_2"
)
object_id
=
'{:024x}'
.
format
(
random
.
randrange
(
16
**
24
))
self
.
assertEqual
(
'edx://'
+
URL_VERSION_PREFIX
+
'chapter12345_2'
,
definition_locator
.
url
())
definition_locator
=
DefinitionLocator
(
object_id
)
self
.
assertEqual
(
'defx://'
+
URL_VERSION_PREFIX
+
object_id
,
definition_locator
.
url
())
self
.
assertEqual
(
definition_locator
,
DefinitionLocator
(
definition_locator
.
url
()))
def
test_description_locator_version
(
self
):
def
test_description_locator_version
(
self
):
definition_locator
=
DefinitionLocator
(
"chapter12345_2"
)
object_id
=
'{:024x}'
.
format
(
random
.
randrange
(
16
**
24
))
self
.
assertEqual
(
"chapter12345_2"
,
definition_locator
.
version
())
definition_locator
=
DefinitionLocator
(
object_id
)
self
.
assertEqual
(
object_id
,
str
(
definition_locator
.
version
()))
# ------------------------------------------------------------------
# ------------------------------------------------------------------
# Utilities
# Utilities
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_split_modulestore.py
View file @
a6ad531d
...
@@ -135,7 +135,7 @@ class SplitModuleCourseTests(SplitModuleTest):
...
@@ -135,7 +135,7 @@ class SplitModuleCourseTests(SplitModuleTest):
self
.
assertEqual
(
self
.
assertEqual
(
len
(
course
.
children
),
3
,
len
(
course
.
children
),
3
,
"children"
)
"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
# check dates and graders--forces loading of descriptor
self
.
assertEqual
(
course
.
edited_by
,
"testassist@edx.org"
)
self
.
assertEqual
(
course
.
edited_by
,
"testassist@edx.org"
)
self
.
assertEqual
(
str
(
course
.
previous_version
),
self
.
GUID_D1
)
self
.
assertEqual
(
str
(
course
.
previous_version
),
self
.
GUID_D1
)
...
@@ -195,7 +195,7 @@ class SplitModuleCourseTests(SplitModuleTest):
...
@@ -195,7 +195,7 @@ class SplitModuleCourseTests(SplitModuleTest):
self
.
assertEqual
(
course
.
graceperiod
,
datetime
.
timedelta
(
hours
=
2
))
self
.
assertEqual
(
course
.
graceperiod
,
datetime
.
timedelta
(
hours
=
2
))
self
.
assertIsNone
(
course
.
advertised_start
)
self
.
assertIsNone
(
course
.
advertised_start
)
self
.
assertEqual
(
len
(
course
.
children
),
0
)
self
.
assertEqual
(
len
(
course
.
children
),
0
)
self
.
assertEqual
(
course
.
definition_locator
.
definition_id
,
"head12345_1
1"
)
self
.
assertEqual
(
str
(
course
.
definition_locator
.
definition_id
),
"ad00000000000000dddd000
1"
)
# check dates and graders--forces loading of descriptor
# check dates and graders--forces loading of descriptor
self
.
assertEqual
(
course
.
edited_by
,
"testassist@edx.org"
)
self
.
assertEqual
(
course
.
edited_by
,
"testassist@edx.org"
)
self
.
assertDictEqual
(
course
.
grade_cutoffs
,
{
"Pass"
:
0.55
})
self
.
assertDictEqual
(
course
.
grade_cutoffs
,
{
"Pass"
:
0.55
})
...
@@ -345,7 +345,7 @@ class SplitModuleItemTests(SplitModuleTest):
...
@@ -345,7 +345,7 @@ class SplitModuleItemTests(SplitModuleTest):
self
.
assertEqual
(
block
.
display_name
,
"The Ancient Greek Hero"
)
self
.
assertEqual
(
block
.
display_name
,
"The Ancient Greek Hero"
)
self
.
assertEqual
(
block
.
advertised_start
,
"Fall 2013"
)
self
.
assertEqual
(
block
.
advertised_start
,
"Fall 2013"
)
self
.
assertEqual
(
len
(
block
.
children
),
3
)
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
# check dates and graders--forces loading of descriptor
self
.
assertEqual
(
block
.
edited_by
,
"testassist@edx.org"
)
self
.
assertEqual
(
block
.
edited_by
,
"testassist@edx.org"
)
self
.
assertDictEqual
(
self
.
assertDictEqual
(
...
@@ -375,7 +375,7 @@ class SplitModuleItemTests(SplitModuleTest):
...
@@ -375,7 +375,7 @@ class SplitModuleItemTests(SplitModuleTest):
block
=
modulestore
()
.
get_item
(
locator
)
block
=
modulestore
()
.
get_item
(
locator
)
self
.
assertEqual
(
block
.
location
.
course_id
,
"GreekHero"
)
self
.
assertEqual
(
block
.
location
.
course_id
,
"GreekHero"
)
self
.
assertEqual
(
block
.
category
,
'chapter'
)
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
.
display_name
,
"Hercules"
)
self
.
assertEqual
(
block
.
edited_by
,
"testassist@edx.org"
)
self
.
assertEqual
(
block
.
edited_by
,
"testassist@edx.org"
)
...
@@ -562,13 +562,13 @@ class TestItemCrud(SplitModuleTest):
...
@@ -562,13 +562,13 @@ class TestItemCrud(SplitModuleTest):
new_module
=
modulestore
()
.
create_item
(
new_module
=
modulestore
()
.
create_item
(
locator
,
category
,
'user123'
,
locator
,
category
,
'user123'
,
fields
=
{
'display_name'
:
'new chapter'
},
fields
=
{
'display_name'
:
'new chapter'
},
definition_locator
=
DefinitionLocator
(
"c
hapter12345_
2"
)
definition_locator
=
DefinitionLocator
(
"c
d00000000000000dddd002
2"
)
)
)
# check that course version changed and course's previous is the other one
# 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
)
self
.
assertNotEqual
(
new_module
.
location
.
version_guid
,
premod_course
.
location
.
version_guid
)
parent
=
modulestore
()
.
get_item
(
locator
)
parent
=
modulestore
()
.
get_item
(
locator
)
self
.
assertIn
(
new_module
.
location
.
usage_id
,
parent
.
children
)
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
),
"cd00000000000000dddd002
2"
)
def
test_unique_naming
(
self
):
def
test_unique_naming
(
self
):
"""
"""
...
@@ -588,7 +588,7 @@ class TestItemCrud(SplitModuleTest):
...
@@ -588,7 +588,7 @@ class TestItemCrud(SplitModuleTest):
another_module
=
modulestore
()
.
create_item
(
another_module
=
modulestore
()
.
create_item
(
locator
,
category
,
'anotheruser'
,
locator
,
category
,
'anotheruser'
,
fields
=
{
'display_name'
:
'problem 2'
,
'data'
:
another_payload
},
fields
=
{
'display_name'
:
'problem 2'
,
'data'
:
another_payload
},
definition_locator
=
DefinitionLocator
(
"
problem12345_3_
1"
),
definition_locator
=
DefinitionLocator
(
"
0d00000040000000dddd003
1"
),
)
)
# check that course version changed and course's previous is the other one
# check that course version changed and course's previous is the other one
parent
=
modulestore
()
.
get_item
(
locator
)
parent
=
modulestore
()
.
get_item
(
locator
)
...
@@ -605,7 +605,7 @@ class TestItemCrud(SplitModuleTest):
...
@@ -605,7 +605,7 @@ class TestItemCrud(SplitModuleTest):
self
.
assertLessEqual
(
new_history
[
'edited_on'
],
datetime
.
datetime
.
now
(
UTC
))
self
.
assertLessEqual
(
new_history
[
'edited_on'
],
datetime
.
datetime
.
now
(
UTC
))
self
.
assertGreaterEqual
(
new_history
[
'edited_on'
],
premod_time
)
self
.
assertGreaterEqual
(
new_history
[
'edited_on'
],
premod_time
)
another_history
=
modulestore
()
.
get_definition_history_info
(
another_module
.
definition_locator
)
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'
]),
'0d00000040000000dddd003
1'
)
def
test_create_continue_version
(
self
):
def
test_create_continue_version
(
self
):
"""
"""
...
@@ -789,7 +789,7 @@ class TestItemCrud(SplitModuleTest):
...
@@ -789,7 +789,7 @@ class TestItemCrud(SplitModuleTest):
modulestore
()
.
create_item
(
modulestore
()
.
create_item
(
locator
,
category
,
'test_update_manifold'
,
locator
,
category
,
'test_update_manifold'
,
fields
=
{
'display_name'
:
'problem 2'
,
'data'
:
another_payload
},
fields
=
{
'display_name'
:
'problem 2'
,
'data'
:
another_payload
},
definition_locator
=
DefinitionLocator
(
"
problem12345_3_
1"
),
definition_locator
=
DefinitionLocator
(
"
0d00000040000000dddd003
1"
),
)
)
# pylint: disable=W0212
# pylint: disable=W0212
modulestore
()
.
_clear_cache
()
modulestore
()
.
_clear_cache
()
...
...
common/test/data/splitmongo_json/definitions.json
View file @
a6ad531d
[
[
{
{
"_id"
:
"head12345_12"
,
"_id"
:
{
"$oid"
:
"ad00000000000000dddd0000"
}
,
"category"
:
"course"
,
"category"
:
"course"
,
"fields"
:{
"fields"
:{
"textbooks"
:[
"textbooks"
:[
...
@@ -46,12 +46,12 @@
...
@@ -46,12 +46,12 @@
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"testassist@edx.org"
,
"edited_by"
:
"testassist@edx.org"
,
"edited_on"
:{
"$date"
:
1364481713238
},
"edited_on"
:{
"$date"
:
1364481713238
},
"previous_version"
:
"head12345_11"
,
"previous_version"
:
{
"$oid"
:
"ad00000000000000dddd0001"
}
,
"original_version"
:
"head12345_10"
"original_version"
:
{
"$oid"
:
"ad00000000000000dddd0010"
}
}
}
},
},
{
{
"_id"
:
"head12345_11"
,
"_id"
:
{
"$oid"
:
"ad00000000000000dddd0001"
}
,
"category"
:
"course"
,
"category"
:
"course"
,
"fields"
:{
"fields"
:{
"textbooks"
:[
"textbooks"
:[
...
@@ -97,12 +97,12 @@
...
@@ -97,12 +97,12 @@
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"testassist@edx.org"
,
"edited_by"
:
"testassist@edx.org"
,
"edited_on"
:{
"$date"
:
1364481713238
},
"edited_on"
:{
"$date"
:
1364481713238
},
"previous_version"
:
"head12345_10"
,
"previous_version"
:
{
"$oid"
:
"ad00000000000000dddd0010"
}
,
"original_version"
:
"head12345_10"
"original_version"
:
{
"$oid"
:
"ad00000000000000dddd0010"
}
}
}
},
},
{
{
"_id"
:
"head12345_10"
,
"_id"
:
{
"$oid"
:
"ad00000000000000dddd0010"
}
,
"category"
:
"course"
,
"category"
:
"course"
,
"fields"
:{
"fields"
:{
"textbooks"
:[
"textbooks"
:[
...
@@ -149,11 +149,11 @@
...
@@ -149,11 +149,11 @@
"edited_by"
:
"test@edx.org"
,
"edited_by"
:
"test@edx.org"
,
"edited_on"
:{
"$date"
:
1364473713238
},
"edited_on"
:{
"$date"
:
1364473713238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"head12345_10"
"original_version"
:
{
"$oid"
:
"ad00000000000000dddd0010"
}
}
}
},
},
{
{
"_id"
:
"head23456_1"
,
"_id"
:
{
"$oid"
:
"ad00000000000000dddd0020"
}
,
"category"
:
"course"
,
"category"
:
"course"
,
"fields"
:{
"fields"
:{
"textbooks"
:[
"textbooks"
:[
...
@@ -199,12 +199,12 @@
...
@@ -199,12 +199,12 @@
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"test@edx.org"
,
"edited_by"
:
"test@edx.org"
,
"edited_on"
:{
"$date"
:
1364481313238
},
"edited_on"
:{
"$date"
:
1364481313238
},
"previous_version"
:
"head23456_0"
,
"previous_version"
:
{
"$oid"
:
"2d00000000000000dddd0020"
}
,
"original_version"
:
"head23456_0"
"original_version"
:
{
"$oid"
:
"2d00000000000000dddd0020"
}
}
}
},
},
{
{
"_id"
:
"head23456_0"
,
"_id"
:
{
"$oid"
:
"2d00000000000000dddd0020"
}
,
"category"
:
"course"
,
"category"
:
"course"
,
"fields"
:{
"fields"
:{
"textbooks"
:[
"textbooks"
:[
...
@@ -251,11 +251,11 @@
...
@@ -251,11 +251,11 @@
"edited_by"
:
"test@edx.org"
,
"edited_by"
:
"test@edx.org"
,
"edited_on"
:{
"$date"
:
1364481313238
},
"edited_on"
:{
"$date"
:
1364481313238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"head23456_0"
"original_version"
:
{
"$oid"
:
"2d00000000000000dddd0020"
}
}
}
},
},
{
{
"_id"
:
"head345679_1"
,
"_id"
:
{
"$oid"
:
"3d00000000000000dddd0020"
}
,
"category"
:
"course"
,
"category"
:
"course"
,
"fields"
:{
"fields"
:{
"textbooks"
:[
"textbooks"
:[
...
@@ -295,62 +295,62 @@
...
@@ -295,62 +295,62 @@
"edited_by"
:
"test@edx.org"
,
"edited_by"
:
"test@edx.org"
,
"edited_on"
:{
"$date"
:
1364481313238
},
"edited_on"
:{
"$date"
:
1364481313238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"head23456_0"
"original_version"
:
{
"$oid"
:
"2d00000000000000dddd0020"
}
}
}
},
},
{
{
"_id"
:
"chapter12345_1"
,
"_id"
:
{
"$oid"
:
"cd00000000000000dddd0020"
}
,
"category"
:
"chapter"
,
"category"
:
"chapter"
,
"fields"
:{},
"fields"
:{},
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"testassist@edx.org"
,
"edited_by"
:
"testassist@edx.org"
,
"edited_on"
:{
"$date"
:
1364483713238
},
"edited_on"
:{
"$date"
:
1364483713238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"chapter12345_1"
"original_version"
:
{
"$oid"
:
"cd00000000000000dddd0020"
}
}
}
},
},
{
{
"_id"
:
"chapter12345_2"
,
"_id"
:
{
"$oid"
:
"cd00000000000000dddd0022"
}
,
"category"
:
"chapter"
,
"category"
:
"chapter"
,
"fields"
:{},
"fields"
:{},
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"testassist@edx.org"
,
"edited_by"
:
"testassist@edx.org"
,
"edited_on"
:{
"$date"
:
1364483713238
},
"edited_on"
:{
"$date"
:
1364483713238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"chapter12345_2"
"original_version"
:
{
"$oid"
:
"cd00000000000000dddd0022"
}
}
}
},
},
{
{
"_id"
:
"chapter12345_3"
,
"_id"
:
{
"$oid"
:
"cd00000000000000dddd0032"
}
,
"category"
:
"chapter"
,
"category"
:
"chapter"
,
"fields"
:{},
"fields"
:{},
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"testassist@edx.org"
,
"edited_by"
:
"testassist@edx.org"
,
"edited_on"
:{
"$date"
:
1364483713238
},
"edited_on"
:{
"$date"
:
1364483713238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"chapter12345_3"
"original_version"
:
{
"$oid"
:
"cd00000000000000dddd0032"
}
}
}
},
},
{
{
"_id"
:
"problem12345_3_1"
,
"_id"
:
{
"$oid"
:
"0d00000040000000dddd0031"
}
,
"category"
:
"problem"
,
"category"
:
"problem"
,
"fields"
:
{
"data"
:
""
},
"fields"
:
{
"data"
:
""
},
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"testassist@edx.org"
,
"edited_by"
:
"testassist@edx.org"
,
"edited_on"
:{
"$date"
:
1364483713238
},
"edited_on"
:{
"$date"
:
1364483713238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"problem12345_3_1"
"original_version"
:
{
"$oid"
:
"0d00000040000000dddd0031"
}
}
}
},
},
{
{
"_id"
:
"problem12345_3_2"
,
"_id"
:
{
"$oid"
:
"0d00000040000000dddd0032"
}
,
"category"
:
"problem"
,
"category"
:
"problem"
,
"fields"
:
{
"data"
:
""
},
"fields"
:
{
"data"
:
""
},
"edit_info"
:
{
"edit_info"
:
{
"edited_by"
:
"testassist@edx.org"
,
"edited_by"
:
"testassist@edx.org"
,
"edited_on"
:{
"$date"
:
1364483713238
},
"edited_on"
:{
"$date"
:
1364483713238
},
"previous_version"
:
null
,
"previous_version"
:
null
,
"original_version"
:
"problem12345_3_2"
"original_version"
:
{
"$oid"
:
"0d00000040000000dddd0032"
}
}
}
}
}
]
]
\ No newline at end of file
common/test/data/splitmongo_json/structures.json
View file @
a6ad531d
...
@@ -11,7 +11,7 @@
...
@@ -11,7 +11,7 @@
"blocks"
:{
"blocks"
:{
"head12345"
:{
"head12345"
:{
"category"
:
"course"
,
"category"
:
"course"
,
"definition"
:
"head12345_12"
,
"definition"
:
{
"$oid"
:
"ad00000000000000dddd0000"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
"chapter1"
,
"chapter1"
,
...
@@ -65,7 +65,7 @@
...
@@ -65,7 +65,7 @@
},
},
"chapter1"
:{
"chapter1"
:{
"category"
:
"chapter"
,
"category"
:
"chapter"
,
"definition"
:
"chapter12345_1"
,
"definition"
:
{
"$oid"
:
"cd00000000000000dddd0020"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -83,7 +83,7 @@
...
@@ -83,7 +83,7 @@
},
},
"chapter2"
:{
"chapter2"
:{
"category"
:
"chapter"
,
"category"
:
"chapter"
,
"definition"
:
"chapter12345_2"
,
"definition"
:
{
"$oid"
:
"cd00000000000000dddd0022"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -101,7 +101,7 @@
...
@@ -101,7 +101,7 @@
},
},
"chapter3"
:{
"chapter3"
:{
"category"
:
"chapter"
,
"category"
:
"chapter"
,
"definition"
:
"chapter12345_3"
,
"definition"
:
{
"$oid"
:
"cd00000000000000dddd0032"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
"problem1"
,
"problem1"
,
...
@@ -120,7 +120,7 @@
...
@@ -120,7 +120,7 @@
},
},
"problem1"
:{
"problem1"
:{
"category"
:
"problem"
,
"category"
:
"problem"
,
"definition"
:
"problem12345_3_1"
,
"definition"
:
{
"$oid"
:
"0d00000040000000dddd0031"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -139,7 +139,7 @@
...
@@ -139,7 +139,7 @@
},
},
"problem3_2"
:{
"problem3_2"
:{
"category"
:
"problem"
,
"category"
:
"problem"
,
"definition"
:
"problem12345_3_2"
,
"definition"
:
{
"$oid"
:
"0d00000040000000dddd0032"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -169,7 +169,7 @@
...
@@ -169,7 +169,7 @@
"blocks"
:{
"blocks"
:{
"head12345"
:{
"head12345"
:{
"category"
:
"course"
,
"category"
:
"course"
,
"definition"
:
"head12345_11"
,
"definition"
:
{
"$oid"
:
"ad00000000000000dddd0001"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -233,7 +233,7 @@
...
@@ -233,7 +233,7 @@
"blocks"
:{
"blocks"
:{
"head12345"
:{
"head12345"
:{
"category"
:
"course"
,
"category"
:
"course"
,
"definition"
:
"head12345_10"
,
"definition"
:
{
"$oid"
:
"ad00000000000000dddd0010"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -287,7 +287,7 @@
...
@@ -287,7 +287,7 @@
"blocks"
:{
"blocks"
:{
"head23456"
:{
"head23456"
:{
"category"
:
"course"
,
"category"
:
"course"
,
"definition"
:
"head23456_1"
,
"definition"
:
{
"$oid"
:
"ad00000000000000dddd0020"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -342,7 +342,7 @@
...
@@ -342,7 +342,7 @@
"blocks"
:{
"blocks"
:{
"head23456"
:{
"head23456"
:{
"category"
:
"course"
,
"category"
:
"course"
,
"definition"
:
"head23456_0"
,
"definition"
:
{
"$oid"
:
"2d00000000000000dddd0020"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -396,7 +396,7 @@
...
@@ -396,7 +396,7 @@
"blocks"
:{
"blocks"
:{
"head23456"
:{
"head23456"
:{
"category"
:
"course"
,
"category"
:
"course"
,
"definition"
:
"head23456_1"
,
"definition"
:
{
"$oid"
:
"ad00000000000000dddd0020"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
@@ -450,7 +450,7 @@
...
@@ -450,7 +450,7 @@
"blocks"
:{
"blocks"
:{
"head345679"
:{
"head345679"
:{
"category"
:
"course"
,
"category"
:
"course"
,
"definition"
:
"head345679_1"
,
"definition"
:
{
"$oid"
:
"3d00000000000000dddd0020"
}
,
"fields"
:{
"fields"
:{
"children"
:[
"children"
:[
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment