Commit 99e7daf7 by Don Mitchell

RESTful refactoring for course_info updates and handouts.

html page and update access use 2 different urls
GET update can get an individual update
STUD-944
parent de8b378d
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
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)
......@@ -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):
......
'''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)
......@@ -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):
......
......@@ -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)
......@@ -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', 'course_handler',
'course_info_updates', 'get_course_settings',
__all__ = ['course_info_handler', 'course_handler', 'course_info_update_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",
......
......@@ -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
}
......@@ -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,
......
......@@ -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
});
......
define(["backbone"], function(Backbone) {
var ModuleInfo = Backbone.Model.extend({
url: function() {return "/module_info/" + this.id;},
urlRoot: "/xblock",
defaults: {
"id": null,
......
<%! 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.urlbase = '${url_base}';
course_updates.url = '${updates_url}';
course_updates.fetch({reset: true});
var course_handouts = new ModuleInfoModel({
id: '${handouts_location}'
id: '${handouts_locator}'
});
course_handouts.urlbase = '${url_base}';
var editor = new CourseInfoEditView({
el: $('.main-wrapper'),
......
......@@ -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>
......
......@@ -34,10 +34,6 @@ urlpatterns = patterns('', # nopep8
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'contentstore.views.preview_dispatch', name='preview_dispatch'),
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'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment