diff --git a/cms/djangoapps/contentstore/course_info_model.py b/cms/djangoapps/contentstore/course_info_model.py
index ef60a0c1b3..b2ee3bf9c8 100644
--- a/cms/djangoapps/contentstore/course_info_model.py
+++ b/cms/djangoapps/contentstore/course_info_model.py
@@ -4,7 +4,6 @@ from xmodule.modulestore.django import modulestore
from lxml import etree
import re
from django.http import HttpResponseBadRequest
-from contentstore.utils import get_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
@@ -14,10 +13,10 @@ def get_course_updates(location):
[{id : location.url() + idx to make unique, date : string, content : html string}]
"""
try:
- course_updates = get_modulestore(location).get_item(location)
+ course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
template = Location(['i4x', 'edx', "templates", 'course_info', "Empty"])
- course_updates = get_modulestore(location).clone_item(template, Location(location))
+ course_updates = modulestore('direct').clone_item(template, Location(location))
# current db rep: {"_id" : locationjson, "definition" : { "data" : "
[
date
content
]"} "metadata" : ignored}
location_base = course_updates.location.url()
@@ -54,7 +53,7 @@ def update_course_updates(location, update, passed_id=None):
into the html structure.
"""
try:
- course_updates = get_modulestore(location).get_item(location)
+ course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
@@ -94,13 +93,13 @@ def update_course_updates(location, update, passed_id=None):
date_element = etree.SubElement(element, "h2")
date_element.text = update['date']
if new_html_parsed is not None:
- element[1] = new_html_parsed
+ element.append(new_html_parsed)
else:
date_element.tail = update['content']
# update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed)
- get_modulestore(location).update_item(location, course_updates.definition['data'])
+ modulestore('direct').update_item(location, course_updates.definition['data'])
return {"id" : passed_id,
"date" : update['date'],
@@ -115,7 +114,7 @@ def delete_course_update(location, update, passed_id):
return HttpResponseBadRequest
try:
- course_updates = get_modulestore(location).get_item(location)
+ course_updates = modulestore('direct').get_item(location)
except ItemNotFoundError:
return HttpResponseBadRequest
@@ -134,7 +133,7 @@ def delete_course_update(location, update, passed_id):
# update db record
course_updates.definition['data'] = etree.tostring(course_html_parsed)
- store = get_modulestore(location)
+ store = modulestore('direct')
store.update_item(location, course_updates.definition['data'])
return get_course_updates(location)
diff --git a/cms/djangoapps/contentstore/management/commands/xlint.py b/cms/djangoapps/contentstore/management/commands/xlint.py
new file mode 100644
index 0000000000..6bc254a1ff
--- /dev/null
+++ b/cms/djangoapps/contentstore/management/commands/xlint.py
@@ -0,0 +1,28 @@
+from django.core.management.base import BaseCommand, CommandError
+from xmodule.modulestore.xml_importer import perform_xlint
+from xmodule.modulestore.django import modulestore
+from xmodule.contentstore.django import contentstore
+
+
+unnamed_modules = 0
+
+
+class Command(BaseCommand):
+ help = \
+ '''
+ Verify the structure of courseware as to it's suitability for import
+ To run test: rake cms:xlint DATA_DIR=../data [COURSE_DIR=content-edx-101 (optional parameter)]
+ '''
+ def handle(self, *args, **options):
+ if len(args) == 0:
+ raise CommandError("import requires at least one argument: [...]")
+
+ data_dir = args[0]
+ if len(args) > 1:
+ course_dirs = args[1:]
+ else:
+ course_dirs = None
+ print "Importing. Data_dir={data}, course_dirs={courses}".format(
+ data=data_dir,
+ courses=course_dirs)
+ perform_xlint(data_dir, course_dirs, load_error_modules=False)
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
new file mode 100644
index 0000000000..cd07e4556d
--- /dev/null
+++ b/cms/djangoapps/contentstore/module_info_model.py
@@ -0,0 +1,83 @@
+import logging
+
+from xmodule.modulestore.exceptions import ItemNotFoundError
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from lxml import etree
+import re
+from django.http import HttpResponseBadRequest, Http404
+
+def get_module_info(store, location, parent_location = None):
+ try:
+ if location.revision is None:
+ module = store.get_item(location)
+ else:
+ module = store.get_item(location)
+ except ItemNotFoundError:
+ raise Http404
+
+ return {
+ 'id': module.location.url(),
+ 'data': module.definition['data'],
+ 'metadata': module.metadata
+ }
+
+def set_module_info(store, location, post_data):
+ module = None
+ isNew = False
+ try:
+ if location.revision is None:
+ module = store.get_item(location)
+ else:
+ module = store.get_item(location)
+ except:
+ pass
+
+ if module is None:
+ # new module at this location
+ # presume that we have an 'Empty' template
+ template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
+ module = store.clone_item(template_location, location)
+ isNew = True
+
+ logging.debug('post = {0}'.format(post_data))
+
+ if post_data.get('data') is not None:
+ data = post_data['data']
+ logging.debug('data = {0}'.format(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 in posted_metadata.keys():
+
+ # let's strip out any metadata fields from the postback which have been identified as system metadata
+ # and therefore should not be user-editable, so we should accept them back from the client
+ if metadata_key in module.system_metadata_fields:
+ del posted_metadata[metadata_key]
+ elif posted_metadata[metadata_key] is None:
+ # remove both from passed in collection as well as the collection read in from the modulestore
+ if metadata_key in module.metadata:
+ del module.metadata[metadata_key]
+ del posted_metadata[metadata_key]
+
+ # overlay the new metadata over the modulestore sourced collection to support partial updates
+ module.metadata.update(posted_metadata)
+
+ # commit to datastore
+ store.update_metadata(location, module.metadata)
diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py
new file mode 100644
index 0000000000..3274477098
--- /dev/null
+++ b/cms/djangoapps/contentstore/tests/factories.py
@@ -0,0 +1,113 @@
+from factory import Factory
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from time import gmtime
+from uuid import uuid4
+from xmodule.timeparse import stringify_time
+
+
+def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
+ return XModuleCourseFactory._create(class_to_create, **kwargs)
+
+def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
+ return XModuleItemFactory._create(class_to_create, **kwargs)
+
+class XModuleCourseFactory(Factory):
+ """
+ Factory for XModule courses.
+ """
+
+ ABSTRACT_FACTORY = True
+ _creation_function = (XMODULE_COURSE_CREATION,)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+
+ template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
+ org = kwargs.get('org')
+ number = kwargs.get('number')
+ display_name = kwargs.get('display_name')
+ location = Location('i4x', org, number,
+ 'course', Location.clean(display_name))
+
+ store = modulestore('direct')
+
+ # Write the data to the mongo datastore
+ new_course = store.clone_item(template, location)
+
+ # This metadata code was copied from cms/djangoapps/contentstore/views.py
+ if display_name is not None:
+ new_course.metadata['display_name'] = display_name
+
+ new_course.metadata['data_dir'] = uuid4().hex
+ new_course.metadata['start'] = stringify_time(gmtime())
+ new_course.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"},
+ {"type": "progress", "name": "Progress"}]
+
+ # Update the data in the mongo datastore
+ store.update_metadata(new_course.location.url(), new_course.own_metadata)
+
+ return new_course
+
+class Course:
+ pass
+
+class CourseFactory(XModuleCourseFactory):
+ FACTORY_FOR = Course
+
+ template = 'i4x://edx/templates/course/Empty'
+ org = 'MITx'
+ number = '999'
+ display_name = 'Robot Super Course'
+
+class XModuleItemFactory(Factory):
+ """
+ Factory for XModule items.
+ """
+
+ ABSTRACT_FACTORY = True
+ _creation_function = (XMODULE_ITEM_CREATION,)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+
+ DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
+
+ parent_location = Location(kwargs.get('parent_location'))
+ template = Location(kwargs.get('template'))
+ display_name = kwargs.get('display_name')
+
+ store = modulestore('direct')
+
+ # This code was based off that in cms/djangoapps/contentstore/views.py
+ parent = store.get_item(parent_location)
+ dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
+
+ new_item = store.clone_item(template, dest_location)
+
+ # TODO: This needs to be deleted when we have proper storage for static content
+ new_item.metadata['data_dir'] = parent.metadata['data_dir']
+
+ # replace the display name with an optional parameter passed in from the caller
+ if display_name is not None:
+ new_item.metadata['display_name'] = display_name
+
+ store.update_metadata(new_item.location.url(), new_item.own_metadata)
+
+ if new_item.location.category not in DETACHED_CATEGORIES:
+ store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
+
+ return new_item
+
+class Item:
+ pass
+
+class ItemFactory(XModuleItemFactory):
+ FACTORY_FOR = Item
+
+ parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
+ template = 'i4x://edx/templates/chapter/Empty'
+ display_name = 'Section One'
\ No newline at end of file
diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index b0bb6c86a1..f5ce0b3692 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -1,7 +1,6 @@
import json
from django.test import TestCase
from django.test.client import Client
-from mock import patch, Mock
from override_settings import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
@@ -9,11 +8,10 @@ from path import path
from student.models import Registration
from django.contrib.auth.models import User
-from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
-from xmodule.modulestore import Location
from xmodule.modulestore.xml_importer import import_from_xml
import copy
+from factories import *
def parse_json(response):
@@ -22,33 +20,33 @@ def parse_json(response):
def user(email):
- '''look up a user by email'''
+ """look up a user by email"""
return User.objects.get(email=email)
def registration(email):
- '''look up registration object by email'''
+ """look up registration object by email"""
return Registration.objects.get(user__email=email)
class ContentStoreTestCase(TestCase):
def _login(self, email, pw):
- '''Login. View should always return 200. The success/fail is in the
- returned json'''
+ """Login. View should always return 200. The success/fail is in the
+ returned json"""
resp = self.client.post(reverse('login_post'),
{'email': email, 'password': pw})
self.assertEqual(resp.status_code, 200)
return resp
def login(self, email, pw):
- '''Login, check that it worked.'''
+ """Login, check that it worked."""
resp = self._login(email, pw)
data = parse_json(resp)
self.assertTrue(data['success'])
return resp
def _create_account(self, username, email, pw):
- '''Try to create an account. No error checking'''
+ """Try to create an account. No error checking"""
resp = self.client.post('/create_account', {
'username': username,
'email': email,
@@ -62,7 +60,7 @@ class ContentStoreTestCase(TestCase):
return resp
def create_account(self, username, email, pw):
- '''Create the account and check that it worked'''
+ """Create the account and check that it worked"""
resp = self._create_account(username, email, pw)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
@@ -74,8 +72,8 @@ class ContentStoreTestCase(TestCase):
return resp
def _activate_user(self, email):
- '''Look up the activation key for the user, then hit the activate view.
- No error checking'''
+ """Look up the activation key for the user, then hit the activate view.
+ No error checking"""
activation_key = registration(email).activation_key
# and now we try to activate
@@ -220,12 +218,6 @@ class ContentStoreTest(TestCase):
'display_name': 'Robot Super Course',
}
- self.section_data = {
- 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
- 'template' : 'i4x://edx/templates/chapter/Empty',
- 'display_name': 'Section One',
- }
-
def tearDown(self):
# Make sure you flush out the test modulestore after the end
# of the last test because otherwise on the next run
@@ -262,6 +254,16 @@ class ContentStoreTest(TestCase):
self.assertEqual(data['ErrMsg'],
'There is already a course defined with the same organization and course number.')
+ def test_create_course_with_bad_organization(self):
+ """Test new course creation - error path for bad organization name"""
+ self.course_data['org'] = 'University of California, Berkeley'
+ resp = self.client.post(reverse('create_new_course'), self.course_data)
+ data = parse_json(resp)
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(data['ErrMsg'],
+ "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
+
def test_course_index_view_with_no_courses(self):
"""Test viewing the index page with no courses"""
# Create a course so there is something to view
@@ -271,20 +273,27 @@ class ContentStoreTest(TestCase):
status_code=200,
html=True)
+ def test_course_factory(self):
+ course = CourseFactory.create()
+ self.assertIsInstance(course, xmodule.course_module.CourseDescriptor)
+
+ def test_item_factory(self):
+ course = CourseFactory.create()
+ item = ItemFactory.create(parent_location=course.location)
+ self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor)
+
def test_course_index_view_with_course(self):
"""Test viewing the index page with an existing course"""
- # Create a course so there is something to view
- resp = self.client.post(reverse('create_new_course'), self.course_data)
+ CourseFactory.create(display_name='Robot Super Educational Course')
resp = self.client.get(reverse('index'))
self.assertContains(resp,
- 'Robot Super Course',
+ 'Robot Super Educational Course',
status_code=200,
html=True)
def test_course_overview_view_with_course(self):
"""Test viewing the course overview page with an existing course"""
- # Create a course so there is something to view
- resp = self.client.post(reverse('create_new_course'), self.course_data)
+ CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
data = {
'org': 'MITx',
@@ -300,8 +309,15 @@ class ContentStoreTest(TestCase):
def test_clone_item(self):
"""Test cloning an item. E.g. creating a new section"""
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- resp = self.client.post(reverse('clone_item'), self.section_data)
+ CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
+
+ section_data = {
+ 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
+ 'template' : 'i4x://edx/templates/chapter/Empty',
+ 'display_name': 'Section One',
+ }
+
+ resp = self.client.post(reverse('clone_item'), section_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
@@ -323,3 +339,4 @@ class ContentStoreTest(TestCase):
def test_edit_unit_full(self):
self.check_edit_unit('full')
+
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index df0b36c920..d2f19802af 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -1,53 +1,59 @@
-from .utils import get_course_location_for_item, get_lms_link_for_item, \
- compute_unit_state, get_date_display, UnitState
-# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
-from PIL import Image
-from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, \
- create_all_course_groups, get_user_by_email, add_user_to_course_group, \
- remove_user_from_course_group, is_user_in_course_group_role, \
- get_users_in_course_group_by_role
-from cache_toolbox.core import del_cached_content
-from collections import defaultdict
-from datetime import datetime
-from django.conf import settings
-from django.contrib.auth.decorators import login_required
-from django.core.context_processors import csrf
-from django.core.exceptions import PermissionDenied
-from django.core.urlresolvers import reverse
-from django.http import HttpResponse, Http404, HttpResponseBadRequest, \
- HttpResponseForbidden
-from django_future.csrf import ensure_csrf_cookie
-from external_auth.views import ssl_login_shortcut
-from functools import partial
-from mitxmako.shortcuts import render_to_response, render_to_string
-from path import path
-from static_replace import replace_urls
from util.json_request import expect_json
-from uuid import uuid4
-from xmodule.contentstore.content import StaticContent
-from xmodule.contentstore.django import contentstore
-from xmodule.error_module import ErrorDescriptor
-from xmodule.errortracker import exc_info_to_str
-from xmodule.exceptions import NotFoundError
-from xmodule.modulestore import Location
-from xmodule.modulestore.django import modulestore
-from xmodule.modulestore.exceptions import ItemNotFoundError
-from xmodule.modulestore.xml_importer import import_from_xml
-from xmodule.timeparse import stringify_time
-from xmodule.x_module import ModuleSystem
-from xmodule_modifiers import replace_static_urls, wrap_xmodule
import json
import logging
import os
-import shutil
import sys
-import tarfile
import time
-from contentstore import course_info_model
-from contentstore.utils import get_modulestore
+import tarfile
+import shutil
+from datetime import datetime
+from collections import defaultdict
+from uuid import uuid4
+from path import path
+
+# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
+from PIL import Image
+
+from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import PermissionDenied
+from django.core.context_processors import csrf
+from django_future.csrf import ensure_csrf_cookie
+from django.core.urlresolvers import reverse
+from django.conf import settings
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
+from xmodule.x_module import ModuleSystem
+from xmodule.error_module import ErrorDescriptor
+from xmodule.errortracker import exc_info_to_str
+from static_replace import replace_urls
+from external_auth.views import ssl_login_shortcut
+
+from mitxmako.shortcuts import render_to_response, render_to_string
+from xmodule.modulestore.django import modulestore
+from xmodule_modifiers import replace_static_urls, wrap_xmodule
+from xmodule.exceptions import NotFoundError
+from functools import partial
+
+from xmodule.contentstore.django import contentstore
+from xmodule.contentstore.content import StaticContent
+
+from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
+from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
+from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
+from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, get_date_display, UnitState, get_course_for_item
+
+from xmodule.modulestore.xml_importer import import_from_xml
+from contentstore.course_info_model import get_course_updates,\
+ update_course_updates, delete_course_update
+from cache_toolbox.core import del_cached_content
+from xmodule.timeparse import stringify_time
+from contentstore.module_info_model import get_module_info, set_module_info
from cms.djangoapps.models.settings.course_details import CourseDetails,\
CourseSettingsEncoder
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
+from cms.djangoapps.contentstore.utils import get_modulestore
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
@@ -474,11 +480,20 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None)
- module.get_html = wrap_xmodule(
- module.get_html,
- module,
- "xmodule_display.html",
- )
+ # cdodge: Special case
+ if module.location.category == 'static_tab':
+ module.get_html = wrap_xmodule(
+ module.get_html,
+ module,
+ "xmodule_tab_display.html",
+ )
+ else:
+ module.get_html = wrap_xmodule(
+ module.get_html,
+ module,
+ "xmodule_display.html",
+ )
+
module.get_html = replace_static_urls(
module.get_html,
module.metadata.get('data_dir', module.location.course),
@@ -905,7 +920,8 @@ def course_info(request, org, course, name, provided_id=None):
'active_tab': 'courseinfo-tab',
'context_course': course_module,
'url_base' : "/" + org + "/" + course + "/",
- 'course_updates' : json.dumps(course_info_model.get_course_updates(location))
+ 'course_updates' : json.dumps(get_course_updates(location)),
+ 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
})
@expect_json
@@ -928,13 +944,38 @@ def course_info_updates(request, org, course, provided_id=None):
real_method = request.method
if request.method == 'GET':
- return HttpResponse(json.dumps(course_info_model.get_course_updates(location)), mimetype="application/json")
+ return HttpResponse(json.dumps(get_course_updates(location)), mimetype="application/json")
elif real_method == 'POST':
- return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
+ # new instance (unless django makes PUT a POST): updates are coming as POST. Not sure why.
+ return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
elif real_method == 'PUT':
- return HttpResponse(json.dumps(course_info_model.update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
- elif real_method == 'DELETE':
- return HttpResponse(json.dumps(course_info_model.delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
+ return HttpResponse(json.dumps(update_course_updates(location, request.POST, provided_id)), mimetype="application/json")
+ elif real_method == 'DELETE': # coming as POST need to pull from Request Header X-HTTP-Method-Override DELETE
+ return HttpResponse(json.dumps(delete_course_update(location, request.POST, provided_id)), mimetype="application/json")
+
+
+@expect_json
+@login_required
+@ensure_csrf_cookie
+def module_info(request, module_location):
+ location = Location(module_location)
+
+ # NB: we're setting Backbone.emulateHTTP to true on the client so everything comes as a post!!!
+ if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
+ real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
+ else:
+ real_method = request.method
+
+ # check that logged in user has permissions to this item
+ if not has_access(request.user, location):
+ raise PermissionDenied()
+
+ if real_method == 'GET':
+ return HttpResponse(json.dumps(get_module_info(get_modulestore(location), location)), mimetype="application/json")
+ elif real_method == 'POST' or real_method == 'PUT':
+ return HttpResponse(json.dumps(set_module_info(get_modulestore(location), location, request.POST)), mimetype="application/json")
+ else:
+ return HttpResponseBadRequest
@login_required
@ensure_csrf_cookie
@@ -1079,7 +1120,10 @@ def create_new_course(request):
number = request.POST.get('number')
display_name = request.POST.get('display_name')
- dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
+ try:
+ dest_location = Location('i4x', org, number, 'course', Location.clean(display_name))
+ except InvalidLocationError as e:
+ return HttpResponse(json.dumps({'ErrMsg': "Unable to create course '" + display_name + "'.\n\n" + e.message}))
# see if the course already exists
existing_course = None
diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py
index b29e170411..5c9be1cf9c 100644
--- a/cms/envs/jasmine.py
+++ b/cms/envs/jasmine.py
@@ -12,7 +12,9 @@ LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
dev_env=True,
- debug=True)
+ debug=True,
+ local_loglevel='ERROR',
+ console_loglevel='ERROR')
PIPELINE_JS['js-test-source'] = {
'source_filenames': sum([
diff --git a/cms/static/client_templates/course_info_handouts.html b/cms/static/client_templates/course_info_handouts.html
new file mode 100644
index 0000000000..958a1c77d6
--- /dev/null
+++ b/cms/static/client_templates/course_info_handouts.html
@@ -0,0 +1,19 @@
+Edit
+
+
%block>
\ No newline at end of file
diff --git a/cms/templates/edit-tabs.html b/cms/templates/edit-tabs.html
index 94c5e38260..41dee15a7a 100644
--- a/cms/templates/edit-tabs.html
+++ b/cms/templates/edit-tabs.html
@@ -20,23 +20,24 @@
Here you can add and manage additional pages for your course. These pages will be added to the primary navigation menu alongside Courseware, Course Info, Discussion, etc.
+
%block>
\ No newline at end of file
diff --git a/cms/templates/index.html b/cms/templates/index.html
index 652acfa0ea..d41bcc23ac 100644
--- a/cms/templates/index.html
+++ b/cms/templates/index.html
@@ -22,14 +22,14 @@