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
c20f3dbc
Commit
c20f3dbc
authored
Nov 13, 2013
by
Don Mitchell
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1569 from edx/dhm/restful_course
restful course_info access
parents
ce6ac653
99e7daf7
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
290 additions
and
274 deletions
+290
-274
cms/djangoapps/contentstore/course_info_model.py
+28
-16
cms/djangoapps/contentstore/module_info_model.py
+0
-75
cms/djangoapps/contentstore/tests/test_contentstore.py
+10
-10
cms/djangoapps/contentstore/tests/test_course_updates.py
+46
-68
cms/djangoapps/contentstore/tests/utils.py
+6
-0
cms/djangoapps/contentstore/views/component.py
+3
-41
cms/djangoapps/contentstore/views/course.py
+58
-38
cms/djangoapps/contentstore/views/item.py
+118
-5
cms/static/coffee/spec/views/course_info_spec.coffee
+1
-0
cms/static/js/collections/course_update.js
+1
-1
cms/static/js/models/module_info.js
+1
-1
cms/templates/course_info.html
+6
-4
cms/templates/widgets/header.html
+7
-6
cms/urls.py
+5
-9
No files found.
cms/djangoapps/contentstore/course_info_model.py
View file @
c20f3dbc
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.django
import
modulestore
from
lxml
import
html
,
etree
import
re
from
django.http
import
HttpResponseBadRequest
import
logging
from
lxml
import
html
,
etree
from
django.http
import
HttpResponseBadRequest
import
django.utils
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.django
import
modulestore
# # TODO store as array of { date, content } and override course_info_module.definition_from_xml
# # This should be in a class which inherits from XmlDescriptor
log
=
logging
.
getLogger
(
__name__
)
def
get_course_updates
(
location
):
def
get_course_updates
(
location
,
provided_id
):
"""
Retrieve the relevant course_info updates and unpack into the model which the client expects:
[{id :
location.url() + idx to make unique
, date : string, content : html string}]
[{id :
index
, date : string, content : html string}]
"""
try
:
course_updates
=
modulestore
(
'direct'
)
.
get_item
(
location
)
...
...
@@ -35,15 +35,23 @@ def get_course_updates(location):
# Confirm that root is <ol>, iterate over <li>, pull out <h2> subs and then rest of val
course_upd_collection
=
[]
provided_id
=
get_idx
(
provided_id
)
if
provided_id
is
not
None
else
None
if
course_html_parsed
.
tag
==
'ol'
:
# 0 is the newest
for
idx
,
update
in
enumerate
(
course_html_parsed
):
if
len
(
update
)
>
0
:
content
=
_course_info_content
(
update
)
# make the id on the client be 1..len w/ 1 being the oldest and len being the newest
course_upd_collection
.
append
({
"id"
:
location_base
+
"/"
+
str
(
len
(
course_html_parsed
)
-
idx
),
"date"
:
update
.
findtext
(
"h2"
),
"content"
:
content
})
computed_id
=
len
(
course_html_parsed
)
-
idx
payload
=
{
"id"
:
computed_id
,
"date"
:
update
.
findtext
(
"h2"
),
"content"
:
content
}
if
provided_id
is
None
:
course_upd_collection
.
append
(
payload
)
elif
provided_id
==
computed_id
:
return
payload
return
course_upd_collection
...
...
@@ -57,7 +65,8 @@ def update_course_updates(location, update, passed_id=None):
try
:
course_updates
=
modulestore
(
'direct'
)
.
get_item
(
location
)
except
ItemNotFoundError
:
return
HttpResponseBadRequest
()
modulestore
(
'direct'
)
.
create_and_save_xmodule
(
location
)
course_updates
=
modulestore
(
'direct'
)
.
get_item
(
location
)
# purely to handle free formed updates not done via editor. Actually kills them, but at least doesn't break.
try
:
...
...
@@ -89,17 +98,17 @@ def update_course_updates(location, update, passed_id=None):
course_html_parsed
[
-
idx
]
=
new_html_parsed
else
:
course_html_parsed
.
insert
(
0
,
new_html_parsed
)
idx
=
len
(
course_html_parsed
)
passed_id
=
course_updates
.
location
.
url
()
+
"/"
+
str
(
idx
)
# update db record
course_updates
.
data
=
html
.
tostring
(
course_html_parsed
)
modulestore
(
'direct'
)
.
update_item
(
location
,
course_updates
.
data
)
return
{
"id"
:
passed_id
,
"date"
:
update
[
'date'
],
"content"
:
_course_info_content
(
new_html_parsed
)}
return
{
"id"
:
idx
,
"date"
:
update
[
'date'
],
"content"
:
_course_info_content
(
new_html_parsed
),
}
def
_course_info_content
(
html_parsed
):
...
...
@@ -115,6 +124,7 @@ def _course_info_content(html_parsed):
return
content
# pylint: disable=unused-argument
def
delete_course_update
(
location
,
update
,
passed_id
):
"""
Delete the given course_info update from the db.
...
...
@@ -150,7 +160,7 @@ def delete_course_update(location, update, passed_id):
store
=
modulestore
(
'direct'
)
store
.
update_item
(
location
,
course_updates
.
data
)
return
get_course_updates
(
location
)
return
get_course_updates
(
location
,
None
)
def
get_idx
(
passed_id
):
...
...
@@ -160,3 +170,5 @@ def get_idx(passed_id):
idx_matcher
=
re
.
search
(
r'.*?/?(\d+)$'
,
passed_id
)
if
idx_matcher
:
return
int
(
idx_matcher
.
group
(
1
))
else
:
return
None
cms/djangoapps/contentstore/module_info_model.py
deleted
100644 → 0
View file @
ce6ac653
from
static_replace
import
replace_static_urls
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
def
get_module_info
(
store
,
location
,
rewrite_static_links
=
False
):
try
:
module
=
store
.
get_item
(
location
)
except
ItemNotFoundError
:
# create a new one
store
.
create_and_save_xmodule
(
location
)
module
=
store
.
get_item
(
location
)
data
=
module
.
data
if
rewrite_static_links
:
# we pass a partially bogus course_id as we don't have the RUN information passed yet
# through the CMS. Also the contentstore is also not RUN-aware at this point in time.
data
=
replace_static_urls
(
module
.
data
,
None
,
course_id
=
module
.
location
.
org
+
'/'
+
module
.
location
.
course
+
'/BOGUS_RUN_REPLACE_WHEN_AVAILABLE'
)
return
{
'id'
:
module
.
location
.
url
(),
'data'
:
data
,
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
# what's the intent here? all metadata incl inherited & namespaced?
'metadata'
:
module
.
xblock_kvs
.
_metadata
}
def
set_module_info
(
store
,
location
,
post_data
):
module
=
None
try
:
module
=
store
.
get_item
(
location
)
except
ItemNotFoundError
:
# new module at this location: almost always used for the course about pages; thus, no parent. (there
# are quite a handful of about page types available for a course and only the overview is pre-created)
store
.
create_and_save_xmodule
(
location
)
module
=
store
.
get_item
(
location
)
if
post_data
.
get
(
'data'
)
is
not
None
:
data
=
post_data
[
'data'
]
store
.
update_item
(
location
,
data
)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection
if
'children'
in
post_data
and
post_data
[
'children'
]
is
not
None
:
children
=
post_data
[
'children'
]
store
.
update_children
(
location
,
children
)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if
post_data
.
get
(
'metadata'
)
is
not
None
:
posted_metadata
=
post_data
[
'metadata'
]
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for
metadata_key
,
value
in
posted_metadata
.
items
():
if
posted_metadata
[
metadata_key
]
is
None
:
# remove both from passed in collection as well as the collection read in from the modulestore
if
module
.
_field_data
.
has
(
module
,
metadata_key
):
module
.
_field_data
.
delete
(
module
,
metadata_key
)
del
posted_metadata
[
metadata_key
]
else
:
module
.
_field_data
.
set
(
module
,
metadata_key
,
value
)
# commit to datastore
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
store
.
update_metadata
(
location
,
module
.
xblock_kvs
.
_metadata
)
cms/djangoapps/contentstore/tests/test_contentstore.py
View file @
c20f3dbc
...
...
@@ -1200,9 +1200,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml
(
module_store
,
'common/test/data/'
,
[
'toy'
])
handout_location
=
Location
([
'i4x'
,
'edX'
,
'toy'
,
'course_info'
,
'handouts'
])
# get the translation
handouts_locator
=
loc_mapper
()
.
translate_location
(
'edX/toy/2012_Fall'
,
handout_location
)
# get module info
resp
=
self
.
client
.
get
_html
(
reverse
(
'module_info'
,
kwargs
=
{
'module_location'
:
handout_location
}
))
# get module info
(json)
resp
=
self
.
client
.
get
(
handouts_locator
.
url_reverse
(
'/xblock'
,
''
))
# make sure we got a successful response
self
.
assertEqual
(
resp
.
status_code
,
200
)
...
...
@@ -1600,10 +1602,7 @@ class ContentStoreTest(ModuleStoreTestCase):
self
.
assertEqual
(
resp
.
status_code
,
200
)
# course info
resp
=
self
.
client
.
get
(
reverse
(
'course_info'
,
kwargs
=
{
'org'
:
loc
.
org
,
'course'
:
loc
.
course
,
'name'
:
loc
.
name
}))
resp
=
self
.
client
.
get
(
new_location
.
url_reverse
(
'course_info'
))
self
.
assertEqual
(
resp
.
status_code
,
200
)
# settings_details
...
...
@@ -1627,14 +1626,15 @@ class ContentStoreTest(ModuleStoreTestCase):
# go look at a subsection page
subsection_location
=
loc
.
replace
(
category
=
'sequential'
,
name
=
'test_sequence'
)
resp
=
self
.
client
.
get_html
(
reverse
(
'edit_subsection'
,
kwargs
=
{
'location'
:
subsection_location
.
url
()}))
resp
=
self
.
client
.
get_html
(
reverse
(
'edit_subsection'
,
kwargs
=
{
'location'
:
subsection_location
.
url
()})
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
# go look at the Edit page
unit_location
=
loc
.
replace
(
category
=
'vertical'
,
name
=
'test_vertical'
)
resp
=
self
.
client
.
get_html
(
reverse
(
'edit_unit'
,
kwargs
=
{
'location'
:
unit_location
.
url
()}))
resp
=
self
.
client
.
get_html
(
reverse
(
'edit_unit'
,
kwargs
=
{
'location'
:
unit_location
.
url
()}))
self
.
assertEqual
(
resp
.
status_code
,
200
)
def
delete_item
(
category
,
name
):
...
...
cms/djangoapps/contentstore/tests/test_course_updates.py
View file @
c20f3dbc
'''unit tests for course_info views and models.'''
from
contentstore.tests.test_course_settings
import
CourseTestCase
from
django.core.urlresolvers
import
reverse
import
json
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
,
loc_mapper
class
CourseUpdateTest
(
CourseTestCase
):
...
...
@@ -15,61 +14,61 @@ class CourseUpdateTest(CourseTestCase):
Does not supply a provided_id.
"""
payload
=
{
'content'
:
content
,
'date'
:
date
}
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
''
})
payload
=
{
'content'
:
content
,
'date'
:
date
}
url
=
update_locator
.
url_reverse
(
'course_info_update/'
)
resp
=
self
.
client
.
ajax_post
(
url
,
payload
)
self
.
assertContains
(
resp
,
''
,
status_code
=
200
)
return
json
.
loads
(
resp
.
content
)
# first get the update to force the creation
url
=
reverse
(
'course_info'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'name'
:
self
.
course
.
location
.
name
})
self
.
client
.
get
(
url
)
course_locator
=
loc_mapper
()
.
translate_location
(
self
.
course
.
location
.
course_id
,
self
.
course
.
location
,
False
,
True
)
resp
=
self
.
client
.
get_html
(
course_locator
.
url_reverse
(
'course_info/'
))
self
.
assertContains
(
resp
,
'Course Updates'
,
status_code
=
200
)
update_locator
=
loc_mapper
()
.
translate_location
(
self
.
course
.
location
.
course_id
,
self
.
course
.
location
.
replace
(
category
=
'course_info'
,
name
=
'updates'
),
False
,
True
)
init_content
=
'<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
content
=
init_content
+
'</iframe>'
payload
=
get_response
(
content
,
'January 8, 2013'
)
self
.
assertHTMLEqual
(
payload
[
'content'
],
content
)
first_update_url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
payload
[
'id'
]})
first_update_url
=
update_locator
.
url_reverse
(
'course_info_update'
,
str
(
payload
[
'id'
]))
content
+=
'<div>div <p>p<br/></p></div>'
payload
[
'content'
]
=
content
# POST requests were coming in w/ these header values causing an error; so, repro error here
resp
=
self
.
client
.
post
(
first_update_url
,
json
.
dumps
(
payload
),
"application/json"
,
HTTP_X_HTTP_METHOD_OVERRIDE
=
"PUT"
,
REQUEST_METHOD
=
"POST"
)
resp
=
self
.
client
.
ajax_post
(
first_update_url
,
payload
,
HTTP_X_HTTP_METHOD_OVERRIDE
=
"PUT"
,
REQUEST_METHOD
=
"POST"
)
self
.
assertHTMLEqual
(
content
,
json
.
loads
(
resp
.
content
)[
'content'
],
"iframe w/ div"
)
# refetch using provided id
refetched
=
self
.
client
.
get_json
(
first_update_url
)
self
.
assertHTMLEqual
(
content
,
json
.
loads
(
refetched
.
content
)[
'content'
],
"get w/ provided id"
)
# now put in an evil update
content
=
'<ol/>'
payload
=
get_response
(
content
,
'January 11, 2013'
)
self
.
assertHTMLEqual
(
content
,
payload
[
'content'
],
"self closing ol"
)
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
''
})
resp
=
self
.
client
.
get
(
url
)
course_update_url
=
update_locator
.
url_reverse
(
'course_info_update/'
)
resp
=
self
.
client
.
get_json
(
course_update_url
)
payload
=
json
.
loads
(
resp
.
content
)
self
.
assertTrue
(
len
(
payload
)
==
2
)
# try json w/o required fields
self
.
assertContains
(
self
.
client
.
post
(
url
,
json
.
dumps
({
'garbage'
:
1
}),
"application/json"
),
'Failed to save'
,
status_code
=
400
)
self
.
assertContains
(
self
.
client
.
ajax_post
(
course_update_url
,
{
'garbage'
:
1
}),
'Failed to save'
,
status_code
=
400
)
# test an update with text in the tail of the header
content
=
'outside <strong>inside</strong> after'
...
...
@@ -77,28 +76,22 @@ class CourseUpdateTest(CourseTestCase):
self
.
assertHTMLEqual
(
content
,
payload
[
'content'
],
"text outside tag"
)
# now try to update a non-existent update
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
'9'
})
content
=
'blah blah'
payload
=
{
'content'
:
content
,
'date'
:
'January 21, 2013'
}
payload
=
{
'content'
:
content
,
'date'
:
'January 21, 2013'
}
self
.
assertContains
(
self
.
client
.
ajax_post
(
url
,
payload
),
'Failed to save'
,
status_code
=
400
)
self
.
client
.
ajax_post
(
course_update_url
+
'/9'
,
payload
),
'Failed to save'
,
status_code
=
400
)
# update w/ malformed html
content
=
'<garbage tag No closing brace to force <span>error</span>'
payload
=
{
'content'
:
content
,
'date'
:
'January 11, 2013'
}
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
''
})
self
.
assertContains
(
self
.
client
.
ajax_post
(
url
,
payload
),
'<garbage'
)
self
.
client
.
ajax_post
(
course_update_url
,
payload
),
'<garbage'
)
# set to valid html which would break an xml parser
content
=
"<p><br><br></p>"
...
...
@@ -106,10 +99,7 @@ class CourseUpdateTest(CourseTestCase):
self
.
assertHTMLEqual
(
content
,
payload
[
'content'
])
# now try to delete a non-existent update
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
'19'
})
self
.
assertContains
(
self
.
client
.
delete
(
url
),
"delete"
,
status_code
=
400
)
self
.
assertContains
(
self
.
client
.
delete
(
course_update_url
+
'/19'
),
"delete"
,
status_code
=
400
)
# now delete a real update
content
=
'blah blah'
...
...
@@ -117,18 +107,11 @@ class CourseUpdateTest(CourseTestCase):
this_id
=
payload
[
'id'
]
self
.
assertHTMLEqual
(
content
,
payload
[
'content'
],
"single iframe"
)
# first count the entries
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
''
})
resp
=
self
.
client
.
get
(
url
)
resp
=
self
.
client
.
get_json
(
course_update_url
)
payload
=
json
.
loads
(
resp
.
content
)
before_delete
=
len
(
payload
)
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
this_id
})
url
=
update_locator
.
url_reverse
(
'course_info_update/'
,
str
(
this_id
))
resp
=
self
.
client
.
delete
(
url
)
payload
=
json
.
loads
(
resp
.
content
)
self
.
assertTrue
(
len
(
payload
)
==
before_delete
-
1
)
...
...
@@ -144,24 +127,19 @@ class CourseUpdateTest(CourseTestCase):
init_content
=
'<iframe width="560" height="315" src="http://www.youtube.com/embed/RocY-Jd93XU" frameborder="0">'
content
=
init_content
+
'</iframe>'
payload
=
{
'content'
:
content
,
'date'
:
'January 8, 2013'
}
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
''
})
payload
=
{
'content'
:
content
,
'date'
:
'January 8, 2013'
}
resp
=
self
.
client
.
ajax_post
(
url
,
payload
)
update_locator
=
loc_mapper
()
.
translate_location
(
self
.
course
.
location
.
course_id
,
location
,
False
,
True
)
course_update_url
=
update_locator
.
url_reverse
(
'course_info_update/'
)
resp
=
self
.
client
.
ajax_post
(
course_update_url
,
payload
)
payload
=
json
.
loads
(
resp
.
content
)
self
.
assertHTMLEqual
(
payload
[
'content'
],
content
)
# now confirm that the bad news and the iframe make up 2 updates
url
=
reverse
(
'course_info_json'
,
kwargs
=
{
'org'
:
self
.
course
.
location
.
org
,
'course'
:
self
.
course
.
location
.
course
,
'provided_id'
:
''
})
resp
=
self
.
client
.
get
(
url
)
resp
=
self
.
client
.
get_json
(
course_update_url
)
payload
=
json
.
loads
(
resp
.
content
)
self
.
assertTrue
(
len
(
payload
)
==
2
)
cms/djangoapps/contentstore/tests/utils.py
View file @
c20f3dbc
...
...
@@ -49,6 +49,12 @@ class AjaxEnabledTestClient(Client):
"""
return
self
.
get
(
path
,
data
or
{},
follow
,
HTTP_ACCEPT
=
"text/html"
,
**
extra
)
def
get_json
(
self
,
path
,
data
=
None
,
follow
=
False
,
**
extra
):
"""
Convenience method for client.get which sets the accept type to json
"""
return
self
.
get
(
path
,
data
or
{},
follow
,
HTTP_ACCEPT
=
"application/json"
,
**
extra
)
@override_settings
(
MODULESTORE
=
TEST_MODULESTORE
)
class
CourseTestCase
(
ModuleStoreTestCase
):
def
setUp
(
self
):
...
...
cms/djangoapps/contentstore/views/component.py
View file @
c20f3dbc
...
...
@@ -21,9 +21,7 @@ from xmodule.modulestore.django import loc_mapper
from
xblock.fields
import
Scope
from
util.json_request
import
expect_json
,
JsonResponse
from
contentstore.module_info_model
import
get_module_info
,
set_module_info
from
contentstore.utils
import
(
get_modulestore
,
get_lms_link_for_item
,
compute_unit_state
,
UnitState
,
get_course_for_item
)
from
contentstore.utils
import
get_lms_link_for_item
,
compute_unit_state
,
UnitState
,
get_course_for_item
from
models.settings.course_grading
import
CourseGradingModel
...
...
@@ -41,7 +39,7 @@ __all__ = ['OPEN_ENDED_COMPONENT_TYPES',
'create_draft'
,
'publish_draft'
,
'unpublish_unit'
,
'module_info'
]
]
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -240,7 +238,7 @@ def edit_unit(request, location):
pass
else
:
log
.
error
(
"Improper format for course advanced keys!
%
"
,
"Improper format for course advanced keys!
%
s
"
,
course_advanced_keys
)
...
...
@@ -393,39 +391,3 @@ def unpublish_unit(request):
_xmodule_recurse
(
item
,
lambda
i
:
modulestore
()
.
unpublish
(
i
.
location
))
return
HttpResponse
()
@expect_json
@require_http_methods
((
"GET"
,
"POST"
,
"PUT"
))
@login_required
@ensure_csrf_cookie
def
module_info
(
request
,
module_location
):
"Get or set information for a module in the modulestore"
location
=
Location
(
module_location
)
# check that logged in user has permissions to this item
if
not
has_access
(
request
.
user
,
location
):
raise
PermissionDenied
()
rewrite_static_links
=
request
.
GET
.
get
(
'rewrite_url_links'
,
'True'
)
in
[
'True'
,
'true'
]
logging
.
debug
(
'rewrite_static_links = {0} {1}'
.
format
(
request
.
GET
.
get
(
'rewrite_url_links'
,
False
),
rewrite_static_links
)
)
# check that logged in user has permissions to this item
if
not
has_access
(
request
.
user
,
location
):
raise
PermissionDenied
()
if
request
.
method
==
'GET'
:
rsp
=
get_module_info
(
get_modulestore
(
location
),
location
,
rewrite_static_links
=
rewrite_static_links
)
elif
request
.
method
in
(
"POST"
,
"PUT"
):
rsp
=
set_module_info
(
get_modulestore
(
location
),
location
,
request
.
json
)
return
JsonResponse
(
rsp
)
cms/djangoapps/contentstore/views/course.py
View file @
c20f3dbc
...
...
@@ -54,8 +54,8 @@ from xmodule.html_module import AboutDescriptor
from
xmodule.modulestore.locator
import
BlockUsageLocator
from
course_creators.views
import
get_course_creator_status
,
add_user_with_status_unrequested
__all__
=
[
'course_info
'
,
'cours
e_handler'
,
'
course_info_updates'
,
'
get_course_settings'
,
__all__
=
[
'course_info
_handler'
,
'course_handler'
,
'course_info_updat
e_handler'
,
'get_course_settings'
,
'course_config_graders_page'
,
'course_config_advanced_page'
,
'course_settings_updates'
,
...
...
@@ -64,6 +64,7 @@ __all__ = ['course_info', 'course_handler',
'create_textbook'
]
# pylint: disable=unused-argument
@login_required
def
course_handler
(
request
,
tag
=
None
,
course_id
=
None
,
branch
=
None
,
version_guid
=
None
,
block
=
None
):
"""
...
...
@@ -299,61 +300,80 @@ def create_new_course(request):
return
JsonResponse
({
'url'
:
new_location
.
url_reverse
(
"course/"
,
""
)})
# pylint: disable=unused-argument
@login_required
@ensure_csrf_cookie
def
course_info
(
request
,
org
,
course
,
name
,
provided_id
=
None
):
@require_http_methods
([
"GET"
])
def
course_info_handler
(
request
,
tag
=
None
,
course_id
=
None
,
branch
=
None
,
version_guid
=
None
,
block
=
None
):
"""
Send models and views as well as html for editing the course info to the
client.
org, course, name: Attributes of the Location for the item to edit
GET
html: return html for editing the course info handouts and updates.
"""
location
=
get_location_and_verify_access
(
request
,
org
,
course
,
name
)
course_location
=
BlockUsageLocator
(
course_id
=
course_id
,
branch
=
branch
,
version_guid
=
version_guid
,
usage_id
=
block
)
course_old_location
=
loc_mapper
()
.
translate_locator_to_location
(
course_location
)
if
'text/html'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
'text/html'
):
if
not
has_access
(
request
.
user
,
course_location
):
raise
PermissionDenied
()
course_module
=
modulestore
()
.
get_item
(
location
)
course_module
=
modulestore
()
.
get_item
(
course_old_
location
)
# get current updates
location
=
Location
([
'i4x'
,
org
,
course
,
'course_info'
,
"updates"
])
handouts_old_location
=
course_old_location
.
replace
(
category
=
'course_info'
,
name
=
'handouts'
)
handouts_locator
=
loc_mapper
()
.
translate_location
(
course_old_location
.
course_id
,
handouts_old_location
,
False
,
True
)
return
render_to_response
(
'course_info.html'
,
{
'context_course'
:
course_module
,
'url_base'
:
"/"
+
org
+
"/"
+
course
+
"/"
,
'course_updates'
:
json
.
dumps
(
get_course_updates
(
location
)),
'handouts_location'
:
Location
([
'i4x'
,
org
,
course
,
'course_info'
,
'handouts'
])
.
url
(),
'base_asset_url'
:
StaticContent
.
get_base_url_path_for_course_assets
(
location
)
+
'/'
}
)
update_location
=
course_old_location
.
replace
(
category
=
'course_info'
,
name
=
'updates'
)
update_locator
=
loc_mapper
()
.
translate_location
(
course_old_location
.
course_id
,
update_location
,
False
,
True
)
@expect_json
@require_http_methods
((
"GET"
,
"POST"
,
"PUT"
,
"DELETE"
))
return
render_to_response
(
'course_info.html'
,
{
'context_course'
:
course_module
,
'updates_url'
:
update_locator
.
url_reverse
(
'course_info_update/'
),
'handouts_locator'
:
handouts_locator
,
'base_asset_url'
:
StaticContent
.
get_base_url_path_for_course_assets
(
course_old_location
)
+
'/'
}
)
else
:
return
HttpResponseBadRequest
(
"Only supports html requests"
)
# pylint: disable=unused-argument
@login_required
@ensure_csrf_cookie
def
course_info_updates
(
request
,
org
,
course
,
provided_id
=
None
):
@require_http_methods
((
"GET"
,
"POST"
,
"PUT"
,
"DELETE"
))
@expect_json
def
course_info_update_handler
(
request
,
tag
=
None
,
course_id
=
None
,
branch
=
None
,
version_guid
=
None
,
block
=
None
,
provided_id
=
None
):
"""
restful CRUD operations on course_info updates.
org, course: Attributes of the Location for the item to edit
provided_id should be none if it's new (create) and a composite of the
update db id + index otherwise.
"""
# ??? No way to check for access permission afaik
# get current updates
location
=
[
'i4x'
,
org
,
course
,
'course_info'
,
"updates"
]
provided_id should be none if it's new (create) and index otherwise.
GET
json: return the course info update models
POST
json: create an update
PUT or DELETE
json: change an existing update
"""
if
'application/json'
not
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
'application/json'
):
return
HttpResponseBadRequest
(
"Only supports json requests"
)
updates_locator
=
BlockUsageLocator
(
course_id
=
course_id
,
branch
=
branch
,
version_guid
=
version_guid
,
usage_id
=
block
)
updates_location
=
loc_mapper
()
.
translate_locator_to_location
(
updates_locator
)
if
provided_id
==
''
:
provided_id
=
None
# check that logged in user has permissions to this item
if
not
has_access
(
request
.
user
,
location
):
# check that logged in user has permissions to this item
(GET shouldn't require this level?)
if
not
has_access
(
request
.
user
,
updates_
location
):
raise
PermissionDenied
()
if
request
.
method
==
'GET'
:
return
JsonResponse
(
get_course_updates
(
location
))
return
JsonResponse
(
get_course_updates
(
updates_location
,
provided_id
))
elif
request
.
method
==
'DELETE'
:
try
:
return
JsonResponse
(
delete_course_update
(
location
,
request
.
json
,
provided_id
))
return
JsonResponse
(
delete_course_update
(
updates_
location
,
request
.
json
,
provided_id
))
except
:
return
HttpResponseBadRequest
(
"Failed to delete"
,
...
...
@@ -362,7 +382,7 @@ def course_info_updates(request, org, course, provided_id=None):
# can be either and sometimes django is rewriting one to the other:
elif
request
.
method
in
(
'POST'
,
'PUT'
):
try
:
return
JsonResponse
(
update_course_updates
(
location
,
request
.
json
,
provided_id
))
return
JsonResponse
(
update_course_updates
(
updates_
location
,
request
.
json
,
provided_id
))
except
:
return
HttpResponseBadRequest
(
"Failed to save"
,
...
...
cms/djangoapps/contentstore/views/item.py
View file @
c20f3dbc
...
...
@@ -3,6 +3,7 @@
import
logging
from
uuid
import
uuid4
from
requests.packages.urllib3.util
import
parse_url
from
static_replace
import
replace_static_urls
from
django.core.exceptions
import
PermissionDenied
,
ValidationError
from
django.contrib.auth.decorators
import
login_required
...
...
@@ -24,6 +25,7 @@ from xmodule.x_module import XModuleDescriptor
from
django.views.decorators.http
import
require_http_methods
from
xmodule.modulestore.locator
import
BlockUsageLocator
from
student.models
import
CourseEnrollment
from
xblock.fields
import
Scope
__all__
=
[
'save_item'
,
'create_item'
,
'orphan'
,
'xblock_handler'
]
...
...
@@ -33,7 +35,8 @@ log = logging.getLogger(__name__)
DETACHED_CATEGORIES
=
[
'about'
,
'static_tab'
,
'course_info'
]
@require_http_methods
((
"DELETE"
))
# pylint: disable=unused-argument
@require_http_methods
((
"DELETE"
,
"GET"
,
"PUT"
,
"POST"
))
@login_required
@expect_json
def
xblock_handler
(
request
,
tag
=
None
,
course_id
=
None
,
branch
=
None
,
version_guid
=
None
,
block
=
None
):
...
...
@@ -44,10 +47,19 @@ def xblock_handler(request, tag=None, course_id=None, branch=None, version_guid=
json: delete this xblock instance from the course. Supports query parameters "recurse" to delete
all children and "all_versions" to delete from all (mongo) versions.
"""
if
request
.
method
==
'DELETE'
:
location
=
BlockUsageLocator
(
course_id
=
course_id
,
branch
=
branch
,
version_guid
=
version_guid
,
usage_id
=
block
)
if
not
has_access
(
request
.
user
,
location
):
raise
PermissionDenied
()
location
=
BlockUsageLocator
(
course_id
=
course_id
,
branch
=
branch
,
version_guid
=
version_guid
,
usage_id
=
block
)
if
not
has_access
(
request
.
user
,
location
):
raise
PermissionDenied
()
if
request
.
method
==
'GET'
:
rewrite_static_links
=
request
.
GET
.
get
(
'rewrite_url_links'
,
'True'
)
in
[
'True'
,
'true'
]
rsp
=
_get_module_info
(
location
,
rewrite_static_links
=
rewrite_static_links
)
return
JsonResponse
(
rsp
)
elif
request
.
method
in
(
"POST"
,
"PUT"
):
# Replace w/ save_item from below
rsp
=
_set_module_info
(
location
,
request
.
json
)
return
JsonResponse
(
rsp
)
elif
request
.
method
==
'DELETE'
:
old_location
=
loc_mapper
()
.
translate_locator_to_location
(
location
)
...
...
@@ -261,3 +273,104 @@ def orphan(request, tag=None, course_id=None, branch=None, version_guid=None, bl
return
JsonResponse
({
'deleted'
:
items
})
else
:
raise
PermissionDenied
()
def
_get_module_info
(
usage_loc
,
rewrite_static_links
=
False
):
"""
metadata, data, id representation of a leaf module fetcher.
:param usage_loc: A BlockUsageLocator
"""
old_location
=
loc_mapper
()
.
translate_locator_to_location
(
usage_loc
)
store
=
get_modulestore
(
old_location
)
try
:
module
=
store
.
get_item
(
old_location
)
except
ItemNotFoundError
:
if
old_location
.
category
in
[
'course_info'
]:
# create a new one
store
.
create_and_save_xmodule
(
old_location
)
module
=
store
.
get_item
(
old_location
)
else
:
raise
data
=
module
.
data
if
rewrite_static_links
:
# we pass a partially bogus course_id as we don't have the RUN information passed yet
# through the CMS. Also the contentstore is also not RUN-aware at this point in time.
data
=
replace_static_urls
(
module
.
data
,
None
,
course_id
=
module
.
location
.
org
+
'/'
+
module
.
location
.
course
+
'/BOGUS_RUN_REPLACE_WHEN_AVAILABLE'
)
return
{
'id'
:
unicode
(
usage_loc
),
'data'
:
data
,
'metadata'
:
module
.
get_explicitly_set_fields_by_scope
(
Scope
.
settings
)
}
def
_set_module_info
(
usage_loc
,
post_data
):
"""
Old metadata, data, id representation leaf module updater.
:param usage_loc: a BlockUsageLocator
:param post_data: the payload with data, metadata, and possibly children (even tho the getter
doesn't support children)
"""
# TODO replace with save_item: differences
# - this doesn't handle nullout
# - this returns the new model
old_location
=
loc_mapper
()
.
translate_locator_to_location
(
usage_loc
)
store
=
get_modulestore
(
old_location
)
module
=
None
try
:
module
=
store
.
get_item
(
old_location
)
except
ItemNotFoundError
:
# new module at this location: almost always used for the course about pages; thus, no parent. (there
# are quite a handful of about page types available for a course and only the overview is pre-created)
store
.
create_and_save_xmodule
(
old_location
)
module
=
store
.
get_item
(
old_location
)
if
post_data
.
get
(
'data'
)
is
not
None
:
data
=
post_data
[
'data'
]
store
.
update_item
(
old_location
,
data
)
else
:
data
=
module
.
get_explicitly_set_fields_by_scope
(
Scope
.
content
)
if
post_data
.
get
(
'children'
)
is
not
None
:
children
=
post_data
[
'children'
]
store
.
update_children
(
old_location
,
children
)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if
post_data
.
get
(
'metadata'
)
is
not
None
:
posted_metadata
=
post_data
[
'metadata'
]
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for
metadata_key
,
value
in
posted_metadata
.
items
():
field
=
module
.
fields
[
metadata_key
]
if
value
is
None
:
# remove both from passed in collection as well as the collection read in from the modulestore
field
.
delete_from
(
module
)
else
:
try
:
value
=
field
.
from_json
(
value
)
except
ValueError
:
return
JsonResponse
({
"error"
:
"Invalid data"
},
400
)
field
.
write_to
(
module
,
value
)
# commit to datastore
metadata
=
module
.
get_explicitly_set_fields_by_scope
(
Scope
.
settings
)
store
.
update_metadata
(
old_location
,
metadata
)
else
:
metadata
=
module
.
get_explicitly_set_fields_by_scope
(
Scope
.
settings
)
return
{
'id'
:
unicode
(
usage_loc
),
'data'
:
data
,
'metadata'
:
metadata
}
cms/static/coffee/spec/views/course_info_spec.coffee
View file @
c20f3dbc
...
...
@@ -34,6 +34,7 @@ define ["js/views/course_info_handout", "js/views/course_info_update", "js/model
@
xhrRestore
=
courseUpdatesXhr
.
restore
@
collection
=
new
CourseUpdateCollection
()
@
collection
.
url
=
'course_info_update/'
@
courseInfoEdit
=
new
CourseInfoUpdateView
({
el
:
$
(
'.course-updates'
),
collection
:
@
collection
,
...
...
cms/static/js/collections/course_update.js
View file @
c20f3dbc
...
...
@@ -4,7 +4,7 @@ define(["backbone", "js/models/course_update"], function(Backbone, CourseUpdateM
collection of updates as [{ date : "month day", content : "html"}]
*/
var
CourseUpdateCollection
=
Backbone
.
Collection
.
extend
({
url
:
function
()
{
return
this
.
urlbase
+
"course_info/updates/"
;},
// instantiator must set url
model
:
CourseUpdateModel
});
...
...
cms/static/js/models/module_info.js
View file @
c20f3dbc
define
([
"backbone"
],
function
(
Backbone
)
{
var
ModuleInfo
=
Backbone
.
Model
.
extend
({
url
:
function
()
{
return
"/module_info/"
+
this
.
id
;}
,
url
Root
:
"/xblock"
,
defaults
:
{
"id"
:
null
,
...
...
cms/templates/course_info.html
View file @
c20f3dbc
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
%
>
<
%
inherit
file=
"base.html"
/>
<
%
namespace
name=
'static'
file=
'static_content.html'
/>
...
...
@@ -17,16 +19,16 @@
<
%
block
name=
"jsextra"
>
<script
type=
"text/javascript"
charset=
"utf-8"
>
require
([
"domReady!"
,
"jquery"
,
"js/collections/course_update"
,
"js/models/module_info"
,
"js/models/course_info"
,
"js/views/course_info_edit"
],
function
(
doc
,
$
,
CourseUpdateCollection
,
ModuleInfoModel
,
CourseInfoModel
,
CourseInfoEditView
)
{
var
course_updates
=
new
CourseUpdateCollection
();
course_updates
.
url
base
=
'${url_base
}'
;
course_updates
.
url
=
'${updates_url
}'
;
course_updates
.
fetch
({
reset
:
true
});
var
course_handouts
=
new
ModuleInfoModel
({
id
:
'${handouts_locat
ion
}'
id
:
'${handouts_locat
or
}'
});
course_handouts
.
urlbase
=
'${url_base}'
;
var
editor
=
new
CourseInfoEditView
({
el
:
$
(
'.main-wrapper'
),
...
...
cms/templates/widgets/header.html
View file @
c20f3dbc
...
...
@@ -16,11 +16,12 @@
<
%
ctx_loc =
context_course.location
location =
loc_mapper().translate_location(ctx_loc.course_id,
ctx_loc
,
False
,
True
)
index_url =
location.url_reverse('course/',
'')
checklists_url =
location.url_reverse('checklists/',
'')
course_team_url =
location.url_reverse('course_team/',
'')
assets_url =
location.url_reverse('assets/',
'')
import_url =
location.url_reverse('import/',
'')
index_url =
location.url_reverse('course/')
checklists_url =
location.url_reverse('checklists/')
course_team_url =
location.url_reverse('course_team/')
assets_url =
location.url_reverse('assets/')
import_url =
location.url_reverse('import/')
course_info_url =
location.url_reverse('course_info/')
export_url =
location.url_reverse('export/',
'')
%
>
<h2
class=
"info-course"
>
...
...
@@ -44,7 +45,7 @@
<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>
<a
href=
"${
course_info_url
}"
>
${_("Updates")}
</a>
</li>
<li
class=
"nav-item nav-course-courseware-pages"
>
<a
href=
"${reverse('edit_tabs', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}"
>
${_("Static Pages")}
</a>
...
...
cms/urls.py
View file @
c20f3dbc
...
...
@@ -34,10 +34,6 @@ urlpatterns = patterns('', # nopep8
url
(
r'^preview/xblock/(?P<usage_id>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>[^/]*))?$'
,
'contentstore.views.preview_handler'
,
name
=
'preview_handler'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/info/(?P<name>[^/]+)$'
,
'contentstore.views.course_info'
,
name
=
'course_info'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$'
,
'contentstore.views.course_info_updates'
,
name
=
'course_info_json'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-details/(?P<name>[^/]+)$'
,
'contentstore.views.get_course_settings'
,
name
=
'settings_details'
),
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings-grading/(?P<name>[^/]+)$'
,
...
...
@@ -66,11 +62,6 @@ urlpatterns = patterns('', # nopep8
url
(
r'^(?P<org>[^/]+)/(?P<course>[^/]+)/textbooks/(?P<name>[^/]+)/(?P<tid>\d[^/]*)$'
,
'contentstore.views.textbook_by_id'
,
name
=
'textbook_by_id'
),
# this is a generic method to return the data/metadata associated with a xmodule
url
(
r'^module_info/(?P<module_location>.*)$'
,
'contentstore.views.module_info'
,
name
=
'module_info'
),
# temporary landing page for a course
url
(
r'^edge/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$'
,
'contentstore.views.landing'
,
name
=
'landing'
),
...
...
@@ -112,6 +103,11 @@ urlpatterns += patterns(
url
(
r'^request_course_creator$'
,
'request_course_creator'
),
# (?ix) == ignore case and verbose (multiline regex)
url
(
r'(?ix)^course_team/{}(/)?(?P<email>.+)?$'
.
format
(
parsers
.
URL_RE_SOURCE
),
'course_team_handler'
),
url
(
r'(?ix)^course_info/{}$'
.
format
(
parsers
.
URL_RE_SOURCE
),
'course_info_handler'
),
url
(
r'(?ix)^course_info_update/{}(/)?(?P<provided_id>\d+)?$'
.
format
(
parsers
.
URL_RE_SOURCE
),
'course_info_update_handler'
),
url
(
r'(?ix)^course($|/){}$'
.
format
(
parsers
.
URL_RE_SOURCE
),
'course_handler'
),
url
(
r'(?ix)^checklists/{}(/)?(?P<checklist_index>\d+)?$'
.
format
(
parsers
.
URL_RE_SOURCE
),
'checklists_handler'
),
url
(
r'(?ix)^orphan/{}$'
.
format
(
parsers
.
URL_RE_SOURCE
),
'orphan'
),
...
...
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