Commit 21135416 by Calen Pennington

Merge remote-tracking branch 'origin/master' into feature/alex/poll-merged

Conflicts:
	.ruby-version
	cms/djangoapps/contentstore/tests/test_contentstore.py
	cms/djangoapps/models/settings/course_metadata.py
	common/lib/xmodule/xmodule/course_module.py
	common/lib/xmodule/xmodule/modulestore/tests/factories.py
parents 40f134ed 90caa65b
1.9.3-p374 1.9.3-p374
\ No newline at end of file
...@@ -10,10 +10,8 @@ from datetime import timedelta ...@@ -10,10 +10,8 @@ from datetime import timedelta
import json import json
from fs.osfs import OSFS from fs.osfs import OSFS
import copy import copy
from mock import Mock from json import loads
from json import dumps, loads
from student.models import Registration
from django.contrib.auth.models import User from django.contrib.auth.models import User
from cms.djangoapps.contentstore.utils import get_modulestore from cms.djangoapps.contentstore.utils import get_modulestore
...@@ -23,13 +21,12 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory ...@@ -23,13 +21,12 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course from xmodule.modulestore.store_utilities import delete_course
from xmodule.modulestore.django import modulestore, _MODULESTORES from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.templates import update_templates
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -65,7 +62,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -65,7 +62,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) self.client.login(username=uname, password=password)
def check_edit_unit(self, test_course_name): def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name]) import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
...@@ -84,8 +80,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -84,8 +80,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_static_tab_reordering(self): def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct') module_store = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering # reverse the ordering
reverse_tabs = [] reverse_tabs = []
...@@ -93,9 +89,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -93,9 +89,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
if tab['type'] == 'static_tab': if tab['type'] == 'static_tab':
reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug'])) reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json") self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs': reverse_tabs}), "application/json")
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# compare to make sure that the tabs information is in the expected order after the server call # compare to make sure that the tabs information is in the expected order after the server call
course_tabs = [] course_tabs = []
...@@ -105,28 +101,60 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -105,28 +101,60 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs) self.assertEqual(reverse_tabs, course_tabs)
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.definition['children'])
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children':'true'}),
"application/json")
found = False
try:
module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
found = True
except ItemNotFoundError:
pass
self.assertFalse(found)
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.definition['children'])
def test_about_overrides(self): def test_about_overrides(self):
''' '''
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
while there is a base definition in /about/effort.html while there is a base definition in /about/effort.html
''' '''
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct') module_store = modulestore('direct')
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None])) effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.data, '6 hours') self.assertEqual(effort.data, '6 hours')
# this one should be in a non-override folder # this one should be in a non-override folder
effort = ms.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None])) effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'end_date', None]))
self.assertEqual(effort.data, 'TBD') self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self): def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct') module_store = modulestore('direct')
cs = contentstore() content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = ms.get_item(source_location) course = module_store.get_item(source_location)
self.assertFalse(course.hide_progress_tab) self.assertFalse(course.hide_progress_tab)
def test_clone_course(self): def test_clone_course(self):
...@@ -145,19 +173,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -145,19 +173,19 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
data = parse_json(resp) data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course') self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
ms = modulestore('direct') module_store = modulestore('direct')
cs = contentstore() content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course') dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
clone_course(ms, cs, source_location, dest_location) clone_course(module_store, content_store, source_location, dest_location)
# now loop through all the units in the course and verify that the clone can render them, which # now loop through all the units in the course and verify that the clone can render them, which
# means the objects are at least present # means the objects are at least present
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0) self.assertGreater(len(items), 0)
clone_items = ms.get_items(Location(['i4x', 'MITx', '999', 'vertical', None])) clone_items = module_store.get_items(Location(['i4x', 'MITx', '999', 'vertical', None]))
self.assertGreater(len(clone_items), 0) self.assertGreater(len(clone_items), 0)
for descriptor in items: for descriptor in items:
new_loc = descriptor.location._replace(org='MITx', course='999') new_loc = descriptor.location._replace(org='MITx', course='999')
...@@ -168,14 +196,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -168,14 +196,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_delete_course(self): def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct') module_store = modulestore('direct')
cs = contentstore() content_store = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
delete_course(ms, cs, location, commit=True) delete_course(module_store, content_store, location, commit=True)
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0) self.assertEqual(len(items), 0)
def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''): def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
...@@ -190,10 +218,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -190,10 +218,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(fs.exists(item.location.name + filename_suffix)) self.assertTrue(fs.exists(item.location.name + filename_suffix))
def test_export_course(self): def test_export_course(self):
ms = modulestore('direct') module_store = modulestore('direct')
cs = contentstore() content_store = contentstore()
import_from_xml(ms, 'common/test/data/', ['full']) import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp_clean()) root_dir = path(mkdtemp_clean())
...@@ -201,24 +229,24 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -201,24 +229,24 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
print 'Exporting to tempdir = {0}'.format(root_dir) print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir # export out to a tempdir
export_to_xml(ms, cs, location, root_dir, 'test_export') export_to_xml(module_store, content_store, location, root_dir, 'test_export')
# check for static tabs # check for static tabs
self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html') self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
# check for custom_tags # check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html') self.verify_content_existence(module_store, root_dir, location, 'info', 'course_info', '.html')
# check for custom_tags # check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template') self.verify_content_existence(module_store, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for graiding_policy.json # check for graiding_policy.json
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012') fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json')) self.assertTrue(fs.exists('grading_policy.json'))
course = ms.get_item(location) course = module_store.get_item(location)
# compare what's on disk compared to what we have in our course # compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json','r') as grading_policy: with fs.open('grading_policy.json', 'r') as grading_policy:
on_disk = loads(grading_policy.read()) on_disk = loads(grading_policy.read())
self.assertEqual(on_disk, course.grading_policy) self.assertEqual(on_disk, course.grading_policy)
...@@ -226,18 +254,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -226,18 +254,18 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertTrue(fs.exists('policy.json')) self.assertTrue(fs.exists('policy.json'))
# compare what's on disk to what we have in the course module # compare what's on disk to what we have in the course module
with fs.open('policy.json','r') as course_policy: with fs.open('policy.json', 'r') as course_policy:
on_disk = loads(course_policy.read()) on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk) self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course)) self.assertEqual(on_disk['course/6.002_Spring_2012'], own_metadata(course))
# remove old course # remove old course
delete_course(ms, cs, location) delete_course(module_store, content_store, location)
# reimport # reimport
import_from_xml(ms, root_dir, ['test_export']) import_from_xml(module_store, root_dir, ['test_export'])
items = ms.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0) self.assertGreater(len(items), 0)
for descriptor in items: for descriptor in items:
print "Checking {0}....".format(descriptor.location.url()) print "Checking {0}....".format(descriptor.location.url())
...@@ -247,11 +275,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -247,11 +275,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
shutil.rmtree(root_dir) shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self): def test_course_handouts_rewrites(self):
ms = modulestore('direct') module_store = modulestore('direct')
cs = contentstore() content_store = contentstore()
# import a test course # import a test course
import_from_xml(ms, 'common/test/data/', ['full']) import_from_xml(module_store, 'common/test/data/', ['full'])
handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts']) handout_location = Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
...@@ -266,33 +294,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -266,33 +294,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf') self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_export_course_with_unknown_metadata(self): def test_export_course_with_unknown_metadata(self):
ms = modulestore('direct') module_store = modulestore('direct')
cs = contentstore() content_store = contentstore()
import_from_xml(ms, 'common/test/data/', ['full']) import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
root_dir = path(mkdtemp_clean()) root_dir = path(mkdtemp_clean())
course = ms.get_item(location) course = module_store.get_item(location)
metadata = own_metadata(course) metadata = own_metadata(course)
# add a bool piece of unknown metadata so we can verify we don't throw an exception # add a bool piece of unknown metadata so we can verify we don't throw an exception
metadata['new_metadata'] = True metadata['new_metadata'] = True
ms.update_metadata(location, metadata) module_store.update_metadata(location, metadata)
print 'Exporting to tempdir = {0}'.format(root_dir) print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir # export out to a tempdir
bExported = False exported = False
try: try:
export_to_xml(ms, cs, location, root_dir, 'test_export') export_to_xml(module_store, content_store, location, root_dir, 'test_export')
bExported = True exported = True
except Exception: except Exception:
pass pass
self.assertTrue(bExported) self.assertTrue(exported)
class ContentStoreTest(ModuleStoreTestCase): class ContentStoreTest(ModuleStoreTestCase):
""" """
...@@ -431,7 +459,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -431,7 +459,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_capa_module(self): def test_capa_module(self):
"""Test that a problem treats markdown specially.""" """Test that a problem treats markdown specially."""
course = CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = { problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course', 'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
...@@ -452,10 +480,10 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -452,10 +480,10 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_import_metadata_with_attempts_empty_string(self): def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple']) import_from_xml(modulestore(), 'common/test/data/', ['simple'])
ms = modulestore('direct') module_store = modulestore('direct')
did_load_item = False did_load_item = False
try: try:
ms.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])) module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
did_load_item = True did_load_item = True
except ItemNotFoundError: except ItemNotFoundError:
pass pass
...@@ -466,10 +494,10 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -466,10 +494,10 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_metadata_inheritance(self): def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
ms = modulestore('direct') module_store = modulestore('direct')
course = ms.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])) course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
verticals = ms.get_items(['i4x', 'edX', 'full', 'vertical', None, None]) verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])
# let's assert on the metadata_inheritance on an existing vertical # let's assert on the metadata_inheritance on an existing vertical
for vertical in verticals: for vertical in verticals:
...@@ -481,13 +509,13 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -481,13 +509,13 @@ class ContentStoreTest(ModuleStoreTestCase):
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page') source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
# crate a new module and add it as a child to a vertical # crate a new module and add it as a child to a vertical
ms.clone_item(source_template_location, new_component_location) module_store.clone_item(source_template_location, new_component_location)
parent = verticals[0] parent = verticals[0]
ms.update_children(parent.location, parent.children + [new_component_location.url()]) module_store.update_children(parent.location, parent.children + [new_component_location.url()])
# flush the cache # flush the cache
ms.get_cached_metadata_inheritance_tree(new_component_location, -1) module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
# check for grace period definition which should be defined at the course level # check for grace period definition which should be defined at the course level
self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod) self.assertEqual(parent.lms.graceperiod, new_module.lms.graceperiod)
...@@ -498,11 +526,11 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -498,11 +526,11 @@ class ContentStoreTest(ModuleStoreTestCase):
# now let's define an override at the leaf node level # now let's define an override at the leaf node level
# #
new_module.lms.graceperiod = timedelta(1) new_module.lms.graceperiod = timedelta(1)
ms.update_metadata(new_module.location, own_metadata(new_module)) module_store.update_metadata(new_module.location, own_metadata(new_module))
# flush the cache and refetch # flush the cache and refetch
ms.get_cached_metadata_inheritance_tree(new_component_location, -1) module_store.get_cached_metadata_inheritance_tree(new_component_location, -1)
new_module = ms.get_item(new_component_location) new_module = module_store.get_item(new_component_location)
self.assertEqual(timedelta(1), new_module.lms.graceperiod) self.assertEqual(timedelta(1), new_module.lms.graceperiod)
...@@ -510,15 +538,15 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -510,15 +538,15 @@ class ContentStoreTest(ModuleStoreTestCase):
class TemplateTestCase(ModuleStoreTestCase): class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self): def test_template_cleanup(self):
ms = modulestore('direct') module_store = modulestore('direct')
# insert a bogus template in the store # insert a bogus template in the store
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus') bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page') source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
ms.clone_item(source_template_location, bogus_template_location) module_store.clone_item(source_template_location, bogus_template_location)
verify_create = ms.get_item(bogus_template_location) verify_create = module_store.get_item(bogus_template_location)
self.assertIsNotNone(verify_create) self.assertIsNotNone(verify_create)
# now run cleanup # now run cleanup
...@@ -527,10 +555,8 @@ class TemplateTestCase(ModuleStoreTestCase): ...@@ -527,10 +555,8 @@ class TemplateTestCase(ModuleStoreTestCase):
# now try to find dangling template, it should not be in DB any longer # now try to find dangling template, it should not be in DB any longer
asserted = False asserted = False
try: try:
verify_create = ms.get_item(bogus_template_location) verify_create = module_store.get_item(bogus_template_location)
except ItemNotFoundError: except ItemNotFoundError:
asserted = True asserted = True
self.assertTrue(asserted) self.assertTrue(asserted)
...@@ -90,12 +90,14 @@ def signup(request): ...@@ -90,12 +90,14 @@ def signup(request):
csrf_token = csrf(request)['csrf_token'] csrf_token = csrf(request)['csrf_token']
return render_to_response('signup.html', {'csrf': csrf_token}) return render_to_response('signup.html', {'csrf': csrf_token})
def old_login_redirect(request): def old_login_redirect(request):
''' '''
Redirect to the active login url. Redirect to the active login url.
''' '''
return redirect('login', permanent=True) return redirect('login', permanent=True)
@ssl_login_shortcut @ssl_login_shortcut
@ensure_csrf_cookie @ensure_csrf_cookie
def login_page(request): def login_page(request):
...@@ -108,6 +110,7 @@ def login_page(request): ...@@ -108,6 +110,7 @@ def login_page(request):
'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE), 'forgot_password_link': "//{base}/#forgot-password-modal".format(base=settings.LMS_BASE),
}) })
def howitworks(request): def howitworks(request):
if request.user.is_authenticated(): if request.user.is_authenticated():
return index(request) return index(request)
...@@ -116,6 +119,7 @@ def howitworks(request): ...@@ -116,6 +119,7 @@ def howitworks(request):
# ==== Views for any logged-in user ================================== # ==== Views for any logged-in user ==================================
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def index(request): def index(request):
...@@ -149,6 +153,7 @@ def index(request): ...@@ -149,6 +153,7 @@ def index(request):
# ==== Views with per-item permissions================================ # ==== Views with per-item permissions================================
def has_access(user, location, role=STAFF_ROLE_NAME): def has_access(user, location, role=STAFF_ROLE_NAME):
''' '''
Return True if user allowed to access this piece of data Return True if user allowed to access this piece of data
...@@ -396,6 +401,7 @@ def preview_component(request, location): ...@@ -396,6 +401,7 @@ def preview_component(request, location):
'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(), 'editor': wrap_xmodule(component.get_html, component, 'xmodule_edit.html')(),
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -636,6 +642,17 @@ def delete_item(request): ...@@ -636,6 +642,17 @@ def delete_item(request):
if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions: if item.location.revision is None and item.location.category == 'vertical' and delete_all_versions:
modulestore('direct').delete_item(item.location) modulestore('direct').delete_item(item.location)
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url()
if item_url in parent.definition["children"]:
parent.definition["children"].remove(item_url)
modulestore('direct').update_children(parent.location, parent.definition["children"])
return HttpResponse() return HttpResponse()
...@@ -709,6 +726,7 @@ def create_draft(request): ...@@ -709,6 +726,7 @@ def create_draft(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def publish_draft(request): def publish_draft(request):
...@@ -738,6 +756,7 @@ def unpublish_unit(request): ...@@ -738,6 +756,7 @@ def unpublish_unit(request):
return HttpResponse() return HttpResponse()
@login_required @login_required
@expect_json @expect_json
def clone_item(request): def clone_item(request):
...@@ -765,8 +784,7 @@ def clone_item(request): ...@@ -765,8 +784,7 @@ def clone_item(request):
return HttpResponse(json.dumps({'id': dest_location.url()})) return HttpResponse(json.dumps({'id': dest_location.url()}))
#@login_required
#@ensure_csrf_cookie
def upload_asset(request, org, course, coursename): def upload_asset(request, org, course, coursename):
''' '''
cdodge: this method allows for POST uploading of files into the course asset library, which will cdodge: this method allows for POST uploading of files into the course asset library, which will
...@@ -828,6 +846,7 @@ def upload_asset(request, org, course, coursename): ...@@ -828,6 +846,7 @@ def upload_asset(request, org, course, coursename):
response['asset_url'] = StaticContent.get_url_path_from_location(content.location) response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
return response return response
''' '''
This view will return all CMS users who are editors for the specified course This view will return all CMS users who are editors for the specified course
''' '''
...@@ -860,6 +879,7 @@ def create_json_response(errmsg = None): ...@@ -860,6 +879,7 @@ def create_json_response(errmsg = None):
return resp return resp
''' '''
This POST-back view will add a user - specified by email - to the list of editors for This POST-back view will add a user - specified by email - to the list of editors for
the specified course the specified course
...@@ -892,6 +912,7 @@ def add_user(request, location): ...@@ -892,6 +912,7 @@ def add_user(request, location):
return create_json_response() return create_json_response()
''' '''
This POST-back view will remove a user - specified by email - from the list of editors for This POST-back view will remove a user - specified by email - from the list of editors for
the specified course the specified course
...@@ -923,6 +944,7 @@ def remove_user(request, location): ...@@ -923,6 +944,7 @@ def remove_user(request, location):
def landing(request, org, course, coursename): def landing(request, org, course, coursename):
return render_to_response('temp-course-landing.html', {}) return render_to_response('temp-course-landing.html', {})
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def static_pages(request, org, course, coursename): def static_pages(request, org, course, coursename):
...@@ -1026,6 +1048,7 @@ def edit_tabs(request, org, course, coursename): ...@@ -1026,6 +1048,7 @@ def edit_tabs(request, org, course, coursename):
'components': components 'components': components
}) })
def not_found(request): def not_found(request):
return render_to_response('error.html', {'error': '404'}) return render_to_response('error.html', {'error': '404'})
...@@ -1061,6 +1084,7 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -1061,6 +1084,7 @@ def course_info(request, org, course, name, provided_id=None):
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() 'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url()
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1158,6 +1182,7 @@ def get_course_settings(request, org, course, name): ...@@ -1158,6 +1182,7 @@ def get_course_settings(request, org, course, name):
"section": "details"}) "section": "details"})
}) })
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def course_config_graders_page(request, org, course, name): def course_config_graders_page(request, org, course, name):
...@@ -1181,6 +1206,7 @@ def course_config_graders_page(request, org, course, name): ...@@ -1181,6 +1206,7 @@ def course_config_graders_page(request, org, course, name):
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder) 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
}) })
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def course_config_advanced_page(request, org, course, name): def course_config_advanced_page(request, org, course, name):
...@@ -1204,6 +1230,7 @@ def course_config_advanced_page(request, org, course, name): ...@@ -1204,6 +1230,7 @@ def course_config_advanced_page(request, org, course, name):
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)), 'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
}) })
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1235,6 +1262,7 @@ def course_settings_updates(request, org, course, name, section): ...@@ -1235,6 +1262,7 @@ def course_settings_updates(request, org, course, name, section):
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder), return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json") mimetype="application/json")
@expect_json @expect_json
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -1360,6 +1388,7 @@ def asset_index(request, org, course, name): ...@@ -1360,6 +1388,7 @@ def asset_index(request, org, course, name):
def edge(request): def edge(request):
return render_to_response('university_profiles/edge.html', {}) return render_to_response('university_profiles/edge.html', {})
@login_required @login_required
@expect_json @expect_json
def create_new_course(request): def create_new_course(request):
...@@ -1412,6 +1441,7 @@ def create_new_course(request): ...@@ -1412,6 +1441,7 @@ def create_new_course(request):
return HttpResponse(json.dumps({'id': new_course.location.url()})) return HttpResponse(json.dumps({'id': new_course.location.url()}))
def initialize_course_tabs(course): def initialize_course_tabs(course):
# set up the default tabs # set up the default tabs
# I've added this because when we add static tabs, the LMS either expects a None for the tabs list or # I've added this because when we add static tabs, the LMS either expects a None for the tabs list or
...@@ -1429,6 +1459,7 @@ def initialize_course_tabs(course): ...@@ -1429,6 +1459,7 @@ def initialize_course_tabs(course):
modulestore('direct').update_metadata(course.location.url(), own_metadata(course)) modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def import_course(request, org, course, name): def import_course(request, org, course, name):
...@@ -1506,6 +1537,7 @@ def import_course(request, org, course, name): ...@@ -1506,6 +1537,7 @@ def import_course(request, org, course, name):
course_module.location.name]) course_module.location.name])
}) })
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
def generate_export_course(request, org, course, name): def generate_export_course(request, org, course, name):
...@@ -1557,6 +1589,7 @@ def export_course(request, org, course, name): ...@@ -1557,6 +1589,7 @@ def export_course(request, org, course, name):
'successful_import_redirect_url': '' 'successful_import_redirect_url': ''
}) })
def event(request): def event(request):
''' '''
A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at A noop to swallow the analytics call so that cms methods don't spook and poor developers looking at
......
...@@ -183,7 +183,7 @@ def evaluator(variables, functions, string, cs=False): ...@@ -183,7 +183,7 @@ def evaluator(variables, functions, string, cs=False):
# 0.33k or -17 # 0.33k or -17
number = (Optional(minus | plus) + inner_number number = (Optional(minus | plus) + inner_number
+ Optional(CaselessLiteral("E") + Optional("-") + number_part) + Optional(CaselessLiteral("E") + Optional((plus | minus)) + number_part)
+ Optional(number_suffix)) + Optional(number_suffix))
number = number.setParseAction(number_parse_action) # Convert to number number = number.setParseAction(number_parse_action) # Convert to number
......
...@@ -366,6 +366,12 @@ class ChoiceGroup(InputTypeBase): ...@@ -366,6 +366,12 @@ class ChoiceGroup(InputTypeBase):
self.choices = self.extract_choices(self.xml) self.choices = self.extract_choices(self.xml)
@classmethod
def get_attributes(cls):
return [Attribute("show_correctness", "always"),
Attribute("submitted_message", "Answer received.")]
def _extra_context(self): def _extra_context(self):
return {'input_type': self.html_input_type, return {'input_type': self.html_input_type,
'choices': self.choices, 'choices': self.choices,
......
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
<div class="indicator_container"> <div class="indicator_container">
% if input_type == 'checkbox' or not value: % if input_type == 'checkbox' or not value:
% if status == 'unsubmitted': % if status == 'unsubmitted' or show_correctness == 'never':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
else: else:
correctness = None correctness = None
%> %>
% if correctness: % if correctness and not show_correctness=='never':
class="choicegroup_${correctness}" class="choicegroup_${correctness}"
% endif % endif
% endif % endif
...@@ -41,4 +41,7 @@ ...@@ -41,4 +41,7 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
</fieldset> </fieldset>
% if show_correctness == "never" and (value or status not in ['unsubmitted']):
<div class="capa_alert">${submitted_message}</div>
%endif
</form> </form>
...@@ -102,6 +102,8 @@ class ChoiceGroupTest(unittest.TestCase): ...@@ -102,6 +102,8 @@ class ChoiceGroupTest(unittest.TestCase):
'choices': [('foil1', '<text>This is foil One.</text>'), 'choices': [('foil1', '<text>This is foil One.</text>'),
('foil2', '<text>This is foil Two.</text>'), ('foil2', '<text>This is foil Two.</text>'),
('foil3', 'This is foil Three.'), ], ('foil3', 'This is foil Three.'), ],
'show_correctness': 'always',
'submitted_message': 'Answer received.',
'name_array_suffix': expected_suffix, # what is this for?? 'name_array_suffix': expected_suffix, # what is this for??
} }
......
...@@ -19,7 +19,7 @@ from capa.xqueue_interface import dateformat ...@@ -19,7 +19,7 @@ from capa.xqueue_interface import dateformat
class ResponseTest(unittest.TestCase): class ResponseTest(unittest.TestCase):
""" Base class for tests of capa responses.""" """ Base class for tests of capa responses."""
xml_factory_class = None xml_factory_class = None
def setUp(self): def setUp(self):
...@@ -43,7 +43,7 @@ class ResponseTest(unittest.TestCase): ...@@ -43,7 +43,7 @@ class ResponseTest(unittest.TestCase):
for input_str in incorrect_answers: for input_str in incorrect_answers:
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1') result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
self.assertEqual(result, 'incorrect', self.assertEqual(result, 'incorrect',
msg="%s should be marked incorrect" % str(input_str)) msg="%s should be marked incorrect" % str(input_str))
class MultiChoiceResponseTest(ResponseTest): class MultiChoiceResponseTest(ResponseTest):
...@@ -61,7 +61,7 @@ class MultiChoiceResponseTest(ResponseTest): ...@@ -61,7 +61,7 @@ class MultiChoiceResponseTest(ResponseTest):
def test_named_multiple_choice_grade(self): def test_named_multiple_choice_grade(self):
problem = self.build_problem(choices=[False, True, False], problem = self.build_problem(choices=[False, True, False],
choice_names=["foil_1", "foil_2", "foil_3"]) choice_names=["foil_1", "foil_2", "foil_3"])
# Ensure that we get the expected grades # Ensure that we get the expected grades
self.assert_grade(problem, 'choice_foil_1', 'incorrect') self.assert_grade(problem, 'choice_foil_1', 'incorrect')
self.assert_grade(problem, 'choice_foil_2', 'correct') self.assert_grade(problem, 'choice_foil_2', 'correct')
...@@ -117,7 +117,7 @@ class ImageResponseTest(ResponseTest): ...@@ -117,7 +117,7 @@ class ImageResponseTest(ResponseTest):
# Anything inside the rectangle (and along the borders) is correct # Anything inside the rectangle (and along the borders) is correct
# Everything else is incorrect # Everything else is incorrect
correct_inputs = ["[12,19]", "[10,10]", "[20,20]", correct_inputs = ["[12,19]", "[10,10]", "[20,20]",
"[10,15]", "[20,15]", "[15,10]", "[15,20]"] "[10,15]", "[20,15]", "[15,10]", "[15,20]"]
incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]"] incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]"]
self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs)
...@@ -259,7 +259,7 @@ class OptionResponseTest(ResponseTest): ...@@ -259,7 +259,7 @@ class OptionResponseTest(ResponseTest):
xml_factory_class = OptionResponseXMLFactory xml_factory_class = OptionResponseXMLFactory
def test_grade(self): def test_grade(self):
problem = self.build_problem(options=["first", "second", "third"], problem = self.build_problem(options=["first", "second", "third"],
correct_option="second") correct_option="second")
# Assert that we get the expected grades # Assert that we get the expected grades
...@@ -374,8 +374,8 @@ class StringResponseTest(ResponseTest): ...@@ -374,8 +374,8 @@ class StringResponseTest(ResponseTest):
hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"), hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
("minnesota", "minn", "The state capital of Minnesota is St. Paul")] ("minnesota", "minn", "The state capital of Minnesota is St. Paul")]
problem = self.build_problem(answer="Michigan", problem = self.build_problem(answer="Michigan",
case_sensitive=False, case_sensitive=False,
hints=hints) hints=hints)
# We should get a hint for Wisconsin # We should get a hint for Wisconsin
...@@ -543,7 +543,7 @@ class ChoiceResponseTest(ResponseTest): ...@@ -543,7 +543,7 @@ class ChoiceResponseTest(ResponseTest):
xml_factory_class = ChoiceResponseXMLFactory xml_factory_class = ChoiceResponseXMLFactory
def test_radio_group_grade(self): def test_radio_group_grade(self):
problem = self.build_problem(choice_type='radio', problem = self.build_problem(choice_type='radio',
choices=[False, True, False]) choices=[False, True, False])
# Check that we get the expected results # Check that we get the expected results
...@@ -601,17 +601,17 @@ class NumericalResponseTest(ResponseTest): ...@@ -601,17 +601,17 @@ class NumericalResponseTest(ResponseTest):
correct_responses = ["4", "4.0", "4.00"] correct_responses = ["4", "4.0", "4.00"]
incorrect_responses = ["", "3.9", "4.1", "0"] incorrect_responses = ["", "3.9", "4.1", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses) self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_grade_decimal_tolerance(self): def test_grade_decimal_tolerance(self):
problem = self.build_problem(question_text="What is 2 + 2 approximately?", problem = self.build_problem(question_text="What is 2 + 2 approximately?",
explanation="The answer is 4", explanation="The answer is 4",
answer=4, answer=4,
tolerance=0.1) tolerance=0.1)
correct_responses = ["4.0", "4.00", "4.09", "3.91"] correct_responses = ["4.0", "4.00", "4.09", "3.91"]
incorrect_responses = ["", "4.11", "3.89", "0"] incorrect_responses = ["", "4.11", "3.89", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses) self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_grade_percent_tolerance(self): def test_grade_percent_tolerance(self):
problem = self.build_problem(question_text="What is 2 + 2 approximately?", problem = self.build_problem(question_text="What is 2 + 2 approximately?",
explanation="The answer is 4", explanation="The answer is 4",
...@@ -642,6 +642,15 @@ class NumericalResponseTest(ResponseTest): ...@@ -642,6 +642,15 @@ class NumericalResponseTest(ResponseTest):
incorrect_responses = ["", "2.11", "1.89", "0"] incorrect_responses = ["", "2.11", "1.89", "0"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses) self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
def test_exponential_answer(self):
problem = self.build_problem(question_text="What 5 * 10?",
explanation="The answer is 50",
answer="5e+1")
correct_responses = ["50", "50.0", "5e1", "5e+1", "50e0", "500e-1"]
incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"]
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
class CustomResponseTest(ResponseTest): class CustomResponseTest(ResponseTest):
from response_xml_factory import CustomResponseXMLFactory from response_xml_factory import CustomResponseXMLFactory
...@@ -667,7 +676,7 @@ class CustomResponseTest(ResponseTest): ...@@ -667,7 +676,7 @@ class CustomResponseTest(ResponseTest):
# The code can also set the global overall_message (str) # The code can also set the global overall_message (str)
# to pass a message that applies to the whole response # to pass a message that applies to the whole response
inline_script = textwrap.dedent(""" inline_script = textwrap.dedent("""
messages[0] = "Test Message" messages[0] = "Test Message"
overall_message = "Overall message" overall_message = "Overall message"
""") """)
problem = self.build_problem(answer=inline_script) problem = self.build_problem(answer=inline_script)
...@@ -687,14 +696,14 @@ class CustomResponseTest(ResponseTest): ...@@ -687,14 +696,14 @@ class CustomResponseTest(ResponseTest):
def test_function_code_single_input(self): def test_function_code_single_input(self):
# For function code, we pass in these arguments: # For function code, we pass in these arguments:
# #
# 'expect' is the expect attribute of the <customresponse> # 'expect' is the expect attribute of the <customresponse>
# #
# 'answer_given' is the answer the student gave (if there is just one input) # 'answer_given' is the answer the student gave (if there is just one input)
# or an ordered list of answers (if there are multiple inputs) # or an ordered list of answers (if there are multiple inputs)
#
# #
# The function should return a dict of the form #
# The function should return a dict of the form
# { 'ok': BOOL, 'msg': STRING } # { 'ok': BOOL, 'msg': STRING }
# #
script = textwrap.dedent(""" script = textwrap.dedent("""
...@@ -727,7 +736,7 @@ class CustomResponseTest(ResponseTest): ...@@ -727,7 +736,7 @@ class CustomResponseTest(ResponseTest):
def test_function_code_multiple_input_no_msg(self): def test_function_code_multiple_input_no_msg(self):
# Check functions also have the option of returning # Check functions also have the option of returning
# a single boolean value # a single boolean value
# If true, mark all the inputs correct # If true, mark all the inputs correct
# If false, mark all the inputs incorrect # If false, mark all the inputs incorrect
script = textwrap.dedent(""" script = textwrap.dedent("""
...@@ -736,7 +745,7 @@ class CustomResponseTest(ResponseTest): ...@@ -736,7 +745,7 @@ class CustomResponseTest(ResponseTest):
answer_given[1] == expect) answer_given[1] == expect)
""") """)
problem = self.build_problem(script=script, cfn="check_func", problem = self.build_problem(script=script, cfn="check_func",
expect="42", num_inputs=2) expect="42", num_inputs=2)
# Correct answer -- expect both inputs marked correct # Correct answer -- expect both inputs marked correct
...@@ -764,10 +773,10 @@ class CustomResponseTest(ResponseTest): ...@@ -764,10 +773,10 @@ class CustomResponseTest(ResponseTest):
# If the <customresponse> has multiple inputs associated with it, # If the <customresponse> has multiple inputs associated with it,
# the check function can return a dict of the form: # the check function can return a dict of the form:
# #
# {'overall_message': STRING, # {'overall_message': STRING,
# 'input_list': [{'ok': BOOL, 'msg': STRING}, ...] } # 'input_list': [{'ok': BOOL, 'msg': STRING}, ...] }
# #
# 'overall_message' is displayed at the end of the response # 'overall_message' is displayed at the end of the response
# #
# 'input_list' contains dictionaries representing the correctness # 'input_list' contains dictionaries representing the correctness
...@@ -784,7 +793,7 @@ class CustomResponseTest(ResponseTest): ...@@ -784,7 +793,7 @@ class CustomResponseTest(ResponseTest):
{'ok': check3, 'msg': 'Feedback 3'} ] } {'ok': check3, 'msg': 'Feedback 3'} ] }
""") """)
problem = self.build_problem(script=script, problem = self.build_problem(script=script,
cfn="check_func", num_inputs=3) cfn="check_func", num_inputs=3)
# Grade the inputs (one input incorrect) # Grade the inputs (one input incorrect)
...@@ -821,11 +830,11 @@ class CustomResponseTest(ResponseTest): ...@@ -821,11 +830,11 @@ class CustomResponseTest(ResponseTest):
check1 = (int(answer_given[0]) == 1) check1 = (int(answer_given[0]) == 1)
check2 = (int(answer_given[1]) == 2) check2 = (int(answer_given[1]) == 2)
check3 = (int(answer_given[2]) == 3) check3 = (int(answer_given[2]) == 3)
return {'ok': (check1 and check2 and check3), return {'ok': (check1 and check2 and check3),
'msg': 'Message text'} 'msg': 'Message text'}
""") """)
problem = self.build_problem(script=script, problem = self.build_problem(script=script,
cfn="check_func", num_inputs=3) cfn="check_func", num_inputs=3)
# Grade the inputs (one input incorrect) # Grade the inputs (one input incorrect)
...@@ -862,7 +871,7 @@ class CustomResponseTest(ResponseTest): ...@@ -862,7 +871,7 @@ class CustomResponseTest(ResponseTest):
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(Exception):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_invalid_dict_exception(self): def test_invalid_dict_exception(self):
# Construct a script that passes back an invalid dict format # Construct a script that passes back an invalid dict format
......
...@@ -174,7 +174,8 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -174,7 +174,8 @@ class CourseDescriptor(SequenceDescriptor):
is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings) is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings) no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings) disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings)
pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", default=None, scope=Scope.settings) pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", scope=Scope.settings)
html_textbooks = List(help="List of dictionaries containing html_textbook configuration", scope=Scope.settings)
remote_gradebook = Object(scope=Scope.settings) remote_gradebook = Object(scope=Scope.settings)
allow_anonymous = Boolean(scope=Scope.settings, default=True) allow_anonymous = Boolean(scope=Scope.settings, default=True)
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False) allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
......
...@@ -85,7 +85,10 @@ class FolditModule(XModule): ...@@ -85,7 +85,10 @@ class FolditModule(XModule):
""" """
from foldit.models import Score from foldit.models import Score
return [(e['username'], e['score']) for e in Score.get_tops_n(10)] leaders = [(e['username'], e['score']) for e in Score.get_tops_n(10)]
leaders.sort(key=lambda x: x[1])
return leaders
def get_html(self): def get_html(self):
""" """
......
...@@ -46,10 +46,10 @@ class XModuleCourseFactory(Factory): ...@@ -46,10 +46,10 @@ class XModuleCourseFactory(Factory):
new_course.start = gmtime() new_course.start = gmtime()
new_course.tabs = [{"type": "courseware"}, new_course.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"}, {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"}, {"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}, {"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}] {"type": "progress", "name": "Progress"}]
# Update the data in the mongo datastore # Update the data in the mongo datastore
store.update_metadata(new_course.location.url(), own_metadata(new_course)) store.update_metadata(new_course.location.url(), own_metadata(new_course))
......
...@@ -119,11 +119,11 @@ def test_equality(): ...@@ -119,11 +119,11 @@ def test_equality():
# All the cleaning functions should do the same thing with these # All the cleaning functions should do the same thing with these
general_pairs = [('', ''), general_pairs = [('', ''),
(' ', '_'), (' ', '_'),
('abc,', 'abc_'), ('abc,', 'abc_'),
('ab fg!@//\\aj', 'ab_fg_aj'), ('ab fg!@//\\aj', 'ab_fg_aj'),
(u"ab\xA9", "ab_"), # no unicode allowed for now (u"ab\xA9", "ab_"), # no unicode allowed for now
] ]
def test_clean(): def test_clean():
...@@ -131,7 +131,7 @@ def test_clean(): ...@@ -131,7 +131,7 @@ def test_clean():
('a:b', 'a_b'), # no colons in non-name components ('a:b', 'a_b'), # no colons in non-name components
('a-b', 'a-b'), # dashes ok ('a-b', 'a-b'), # dashes ok
('a.b', 'a.b'), # dot ok ('a.b', 'a.b'), # dot ok
] ]
for input, output in pairs: for input, output in pairs:
assert_equals(Location.clean(input), output) assert_equals(Location.clean(input), output)
...@@ -141,17 +141,17 @@ def test_clean_for_url_name(): ...@@ -141,17 +141,17 @@ def test_clean_for_url_name():
('a:b', 'a:b'), # colons ok in names ('a:b', 'a:b'), # colons ok in names
('a-b', 'a-b'), # dashes ok in names ('a-b', 'a-b'), # dashes ok in names
('a.b', 'a.b'), # dot ok in names ('a.b', 'a.b'), # dot ok in names
] ]
for input, output in pairs: for input, output in pairs:
assert_equals(Location.clean_for_url_name(input), output) assert_equals(Location.clean_for_url_name(input), output)
def test_clean_for_html(): def test_clean_for_html():
pairs = general_pairs + [ pairs = general_pairs + [
("a:b", "a_b"), # no colons for html use ("a:b", "a_b"), # no colons for html use
("a-b", "a-b"), # dashes ok (though need to be replaced in various use locations. ugh.) ("a-b", "a-b"), # dashes ok (though need to be replaced in various use locations. ugh.)
('a.b', 'a_b'), # no dots. ('a.b', 'a_b'), # no dots.
] ]
for input, output in pairs: for input, output in pairs:
assert_equals(Location.clean_for_html(input), output) assert_equals(Location.clean_for_html(input), output)
......
...@@ -12,7 +12,7 @@ def check_path_to_location(modulestore): ...@@ -12,7 +12,7 @@ def check_path_to_location(modulestore):
("edX/toy/2012_Fall", "Overview", "Welcome", None)), ("edX/toy/2012_Fall", "Overview", "Welcome", None)),
("i4x://edX/toy/chapter/Overview", ("i4x://edX/toy/chapter/Overview",
("edX/toy/2012_Fall", "Overview", None, None)), ("edX/toy/2012_Fall", "Overview", None, None)),
) )
course_id = "edX/toy/2012_Fall" course_id = "edX/toy/2012_Fall"
for location, expected in should_work: for location, expected in should_work:
...@@ -20,6 +20,6 @@ def check_path_to_location(modulestore): ...@@ -20,6 +20,6 @@ def check_path_to_location(modulestore):
not_found = ( not_found = (
"i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome" "i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome"
) )
for location in not_found: for location in not_found:
assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location) assert_raises(ItemNotFoundError, path_to_location, modulestore, course_id, location)
...@@ -38,12 +38,15 @@ pip install -q -r test-requirements.txt ...@@ -38,12 +38,15 @@ pip install -q -r test-requirements.txt
yes w | pip install -q -r requirements.txt yes w | pip install -q -r requirements.txt
rake clobber rake clobber
rake pep8
rake pylint
TESTS_FAILED=0 TESTS_FAILED=0
rake test_cms[false] || TESTS_FAILED=1 rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1 rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1
# Don't run the lms jasmine tests for now because # Don't run the lms jasmine tests for now because
# they mostly all fail anyhow # they mostly all fail anyhow
# rake phantomjs_jasmine_lms || true # rake phantomjs_jasmine_lms || true
rake phantomjs_jasmine_cms || TESTS_FAILED=1 rake phantomjs_jasmine_cms || TESTS_FAILED=1
......
...@@ -131,6 +131,17 @@ def _pdf_textbooks(tab, user, course, active_page): ...@@ -131,6 +131,17 @@ def _pdf_textbooks(tab, user, course, active_page):
for index, textbook in enumerate(course.pdf_textbooks)] for index, textbook in enumerate(course.pdf_textbooks)]
return [] return []
def _html_textbooks(tab, user, course, active_page):
"""
Generates one tab per textbook. Only displays if user is authenticated.
"""
if user.is_authenticated():
# since there can be more than one textbook, active_page is e.g. "book/0".
return [CourseTab(textbook['tab_title'], reverse('html_book', args=[course.id, index]),
active_page == "htmltextbook/{0}".format(index))
for index, textbook in enumerate(course.html_textbooks)]
return []
def _staff_grading(tab, user, course, active_page): def _staff_grading(tab, user, course, active_page):
if has_access(user, course, 'staff'): if has_access(user, course, 'staff'):
link = reverse('staff_grading', args=[course.id]) link = reverse('staff_grading', args=[course.id])
...@@ -210,6 +221,7 @@ VALID_TAB_TYPES = { ...@@ -210,6 +221,7 @@ VALID_TAB_TYPES = {
'external_link': TabImpl(key_checker(['name', 'link']), _external_link), 'external_link': TabImpl(key_checker(['name', 'link']), _external_link),
'textbooks': TabImpl(null_validator, _textbooks), 'textbooks': TabImpl(null_validator, _textbooks),
'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks), 'pdf_textbooks': TabImpl(null_validator, _pdf_textbooks),
'html_textbooks': TabImpl(null_validator, _html_textbooks),
'progress': TabImpl(need_name, _progress), 'progress': TabImpl(need_name, _progress),
'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab), 'static_tab': TabImpl(key_checker(['name', 'url_slug']), _static_tab),
'peer_grading': TabImpl(null_validator, _peer_grading), 'peer_grading': TabImpl(null_validator, _peer_grading),
......
...@@ -22,7 +22,6 @@ import pystache_custom as pystache ...@@ -22,7 +22,6 @@ import pystache_custom as pystache
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.search import path_to_location
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -170,7 +169,6 @@ def initialize_discussion_info(course): ...@@ -170,7 +169,6 @@ def initialize_discussion_info(course):
# get all discussion models within this course_id # get all discussion models within this course_id
all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id) all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
path_to_locations = {}
for module in all_modules: for module in all_modules:
skip_module = False skip_module = False
for key in ('discussion_id', 'discussion_category', 'discussion_target'): for key in ('discussion_id', 'discussion_category', 'discussion_target'):
...@@ -178,14 +176,6 @@ def initialize_discussion_info(course): ...@@ -178,14 +176,6 @@ def initialize_discussion_info(course):
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location)) log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
skip_module = True skip_module = True
# cdodge: pre-compute the path_to_location. Note this can throw an exception for any
# dangling discussion modules
try:
path_to_locations[module.location] = path_to_location(modulestore(), course.id, module.location)
except NoPathToItem:
log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location))
skip_module = True
if skip_module: if skip_module:
continue continue
...@@ -248,7 +238,6 @@ def initialize_discussion_info(course): ...@@ -248,7 +238,6 @@ def initialize_discussion_info(course):
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map _DISCUSSIONINFO[course.id]['category_map'] = category_map
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now() _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now()
_DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations
class JsonResponse(HttpResponse): class JsonResponse(HttpResponse):
...@@ -405,21 +394,8 @@ def get_courseware_context(content, course): ...@@ -405,21 +394,8 @@ def get_courseware_context(content, course):
location = id_map[id]["location"].url() location = id_map[id]["location"].url()
title = id_map[id]["title"] title = id_map[id]["title"]
# cdodge: did we pre-compute, if so, then let's use that rather than recomputing url = reverse('jump_to', kwargs={"course_id":course.location.course_id,
if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']: "location": location})
(course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location]
else:
try:
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
except NoPathToItem:
# Object is not in the graph any longer, let's just get path to the base of the course
# so that we can at least return something to the caller
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, course.location)
url = reverse('courseware_position', kwargs={"course_id":course_id,
"chapter":chapter,
"section":section,
"position":position})
content_info = {"courseware_url": url, "courseware_title": title} content_info = {"courseware_url": url, "courseware_title": title}
return content_info return content_info
......
...@@ -59,7 +59,7 @@ class Score(models.Model): ...@@ -59,7 +59,7 @@ class Score(models.Model):
scores = Score.objects \ scores = Score.objects \
.filter(puzzle_id__in=puzzles) \ .filter(puzzle_id__in=puzzles) \
.annotate(total_score=models.Sum('best_score')) \ .annotate(total_score=models.Sum('best_score')) \
.order_by('-total_score')[:n] .order_by('total_score')[:n]
num = len(puzzles) num = len(puzzles)
return [{'username': s.user.username, return [{'username': s.user.username,
......
...@@ -143,11 +143,12 @@ class FolditTestCase(TestCase): ...@@ -143,11 +143,12 @@ class FolditTestCase(TestCase):
def test_SetPlayerPuzzleScores_manyplayers(self): def test_SetPlayerPuzzleScores_manyplayers(self):
""" """
Check that when we send scores from multiple users, the correct order Check that when we send scores from multiple users, the correct order
of scores is displayed. of scores is displayed. Note that, before being processed by
display_score, lower scores are better.
""" """
puzzle_id = ['1'] puzzle_id = ['1']
player1_score = 0.07 player1_score = 0.08
player2_score = 0.08 player2_score = 0.02
response1 = self.make_puzzle_score_request(puzzle_id, player1_score, response1 = self.make_puzzle_score_request(puzzle_id, player1_score,
self.user) self.user)
...@@ -164,8 +165,12 @@ class FolditTestCase(TestCase): ...@@ -164,8 +165,12 @@ class FolditTestCase(TestCase):
self.assertEqual(len(top_10), 2) self.assertEqual(len(top_10), 2)
# Top score should be player2_score. Second should be player1_score # Top score should be player2_score. Second should be player1_score
self.assertEqual(top_10[0]['score'], Score.display_score(player2_score)) self.assertAlmostEqual(top_10[0]['score'],
self.assertEqual(top_10[1]['score'], Score.display_score(player1_score)) Score.display_score(player2_score),
delta=0.5)
self.assertAlmostEqual(top_10[1]['score'],
Score.display_score(player1_score),
delta=0.5)
# Top score user should be self.user2.username # Top score user should be self.user2.username
self.assertEqual(top_10[0]['username'], self.user2.username) self.assertEqual(top_10[0]['username'], self.user2.username)
......
from lxml import etree from lxml import etree
# from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import Http404
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from courseware.access import has_access from courseware.access import has_access
...@@ -15,6 +15,8 @@ def index(request, course_id, book_index, page=None): ...@@ -15,6 +15,8 @@ def index(request, course_id, book_index, page=None):
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
book_index = int(book_index) book_index = int(book_index)
if book_index < 0 or book_index >= len(course.textbooks):
raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.textbooks[book_index] textbook = course.textbooks[book_index]
table_of_contents = textbook.table_of_contents table_of_contents = textbook.table_of_contents
...@@ -40,6 +42,8 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -40,6 +42,8 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
book_index = int(book_index) book_index = int(book_index)
if book_index < 0 or book_index >= len(course.pdf_textbooks):
raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.pdf_textbooks[book_index] textbook = course.pdf_textbooks[book_index]
def remap_static_url(original_url, course): def remap_static_url(original_url, course):
...@@ -67,3 +71,39 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None): ...@@ -67,3 +71,39 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
'chapter': chapter, 'chapter': chapter,
'page': page, 'page': page,
'staff_access': staff_access}) 'staff_access': staff_access})
@login_required
def html_index(request, course_id, book_index, chapter=None, anchor_id=None):
course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff')
book_index = int(book_index)
if book_index < 0 or book_index >= len(course.html_textbooks):
raise Http404("Invalid book index value: {0}".format(book_index))
textbook = course.html_textbooks[book_index]
def remap_static_url(original_url, course):
input_url = "'" + original_url + "'"
output_url = replace_static_urls(
input_url,
course.metadata['data_dir'],
course_namespace=course.location
)
# strip off the quotes again...
return output_url[1:-1]
if 'url' in textbook:
textbook['url'] = remap_static_url(textbook['url'], course)
# then remap all the chapter URLs as well, if they are provided.
if 'chapters' in textbook:
for entry in textbook['chapters']:
entry['url'] = remap_static_url(entry['url'], course)
return render_to_response('static_htmlbook.html',
{'book_index': book_index,
'course': course,
'textbook': textbook,
'chapter': chapter,
'anchor_id': anchor_id,
'staff_access': staff_access})
...@@ -158,6 +158,19 @@ div.book-wrapper { ...@@ -158,6 +158,19 @@ div.book-wrapper {
img { img {
max-width: 100%; max-width: 100%;
} }
div {
text-align: left;
line-height: 1.6em;
margin-left: 5px;
margin-right: 5px;
margin-top: 5px;
margin-bottom: 5px;
.Paragraph, h2 {
margin-top: 10px;
}
}
} }
} }
......
<%inherit file="main.html" />
<%namespace name='static' file='static_content.html'/>
<%block name="title"><title>${course.number} Textbook</title>
</%block>
<%block name="headextra">
<%static:css group='course'/>
<%static:js group='courseware'/>
</%block>
<%block name="js_extra">
<script type="text/javascript">
(function($) {
$.fn.myHTMLViewer = function(options) {
var urlToLoad = null;
if (options.url) {
urlToLoad = options.url;
}
var chapterUrls = null;
if (options.chapters) {
chapterUrls = options.chapters;
}
var chapterToLoad = 1;
if (options.chapterNum) {
// TODO: this should only be specified if there are
// chapters, and it should be in-bounds.
chapterToLoad = options.chapterNum;
}
var anchorToLoad = null;
if (options.chapters) {
anchorToLoad = options.anchor_id;
}
loadUrl = function htmlViewLoadUrl(url, anchorId) {
// clear out previous load, if any:
parentElement = document.getElementById('bookpage');
while (parentElement.hasChildNodes())
parentElement.removeChild(parentElement.lastChild);
// load new URL in:
$('#bookpage').load(url);
// if there is an anchor set, then go to that location:
if (anchorId != null) {
// TODO: add implementation....
}
};
loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum, anchorId) {
if (chapterNum < 1 || chapterNum > chapterUrls.length) {
return;
}
var chapterUrl = chapterUrls[chapterNum-1];
loadUrl(chapterUrl, anchorId);
};
// define navigation links for chapters:
if (chapterUrls != null) {
var loadChapterUrlHelper = function(i) {
return function(event) {
// when opening a new chapter, always open to the top:
loadChapterUrl(i, null);
};
};
for (var index = 1; index <= chapterUrls.length; index += 1) {
$("#htmlchapter-" + index).click(loadChapterUrlHelper(index));
}
}
// finally, load the appropriate url/page
if (urlToLoad != null) {
loadUrl(urlToLoad, anchorToLoad);
} else {
loadChapterUrl(chapterToLoad, anchorToLoad);
}
}
})(jQuery);
$(document).ready(function() {
var options = {};
%if 'url' in textbook:
options.url = "${textbook['url']}";
%endif
%if 'chapters' in textbook:
var chptrs = [];
%for chap in textbook['chapters']:
chptrs.push("${chap['url']}");
%endfor
options.chapters = chptrs;
%endif
%if chapter is not None:
options.chapterNum = ${chapter};
%endif
%if anchor_id is not None:
options.anchor_id = ${anchor_id};
%endif
$('#outerContainer').myHTMLViewer(options);
});
</script>
</%block>
<%include file="/courseware/course_navigation.html" args="active_page='htmltextbook/{0}'.format(book_index)" />
<div id="outerContainer">
<div id="mainContainer" class="book-wrapper">
%if 'chapters' in textbook:
<section aria-label="Textbook Navigation" class="book-sidebar">
<ul id="booknav" class="treeview-booknav">
<%def name="print_entry(entry, index_value)">
<li id="htmlchapter-${index_value}">
<a class="chapter">
${entry.get('title')}
</a>
</li>
</%def>
%for (index, entry) in enumerate(textbook['chapters']):
${print_entry(entry, index+1)}
% endfor
</ul>
</section>
%endif
<section id="viewerContainer" class="book">
<section class="page">
<div id="bookpage" />
</section>
</section>
</div>
</div>
...@@ -280,6 +280,15 @@ if settings.COURSEWARE_ENABLED: ...@@ -280,6 +280,15 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<page>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/pdfbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<page>[^/]*)$',
'staticbook.views.pdf_index'), 'staticbook.views.pdf_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/$',
'staticbook.views.html_index', name="html_book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/chapter/(?P<chapter>[^/]*)/(?P<anchor_id>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/htmlbook/(?P<book_index>[^/]*)/(?P<anchor_id>[^/]*)/$',
'staticbook.views.html_index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"), 'courseware.views.index', name="courseware"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/$',
......
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