Merge remote-tracking branch 'origin/master' into fix/vik/sa-and-coe
This commit is contained in:
@@ -35,6 +35,7 @@ load-plugins=
|
||||
# it should appear only once).
|
||||
disable=
|
||||
# C0301: Line too long
|
||||
# C0302: Too many lines in module
|
||||
# W0141: Used builtin function 'map'
|
||||
# W0142: Used * or ** magic
|
||||
# R0201: Method could be a function
|
||||
@@ -42,8 +43,11 @@ disable=
|
||||
# R0902: Too many instance attributes
|
||||
# R0903: Too few public methods (1/2)
|
||||
# R0904: Too many public methods
|
||||
# R0911: Too many return statements
|
||||
# R0912: Too many branches
|
||||
# R0913: Too many arguments
|
||||
C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
|
||||
# R0914: Too many local variables
|
||||
C0301,C0302,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0911,R0912,R0913,R0914
|
||||
|
||||
|
||||
[REPORTS]
|
||||
@@ -92,7 +96,7 @@ zope=no
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size
|
||||
generated-members=REQUEST,acl_users,aq_parent,objects,DoesNotExist,can_read,can_write,get_url,size,content
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
@@ -22,5 +22,4 @@ libreadline6
|
||||
libreadline6-dev
|
||||
mongodb
|
||||
nodejs
|
||||
npm
|
||||
coffeescript
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
@@ -131,7 +128,7 @@ def remove_user_from_course_group(caller, user, location, role):
|
||||
raise PermissionDenied
|
||||
|
||||
# see if the user is actually in that role, if not then we don't have to do anything
|
||||
if is_user_in_course_group_role(user, location, role) == True:
|
||||
if is_user_in_course_group_role(user, location, role):
|
||||
groupname = get_course_groupname_for_role(location, role)
|
||||
|
||||
group = Group.objects.get(name=groupname)
|
||||
|
||||
@@ -97,8 +97,7 @@ def update_course_updates(location, update, passed_id=None):
|
||||
if (len(new_html_parsed) == 1):
|
||||
content = new_html_parsed[0].tail
|
||||
else:
|
||||
content = "\n".join([html.tostring(ele)
|
||||
for ele in new_html_parsed[1:]])
|
||||
content = "\n".join([html.tostring(ele) for ele in new_html_parsed[1:]])
|
||||
|
||||
return {"id": passed_id,
|
||||
"date": update['date'],
|
||||
|
||||
@@ -6,6 +6,7 @@ from nose.tools import assert_true, assert_equal
|
||||
from terrain.steps import reload_the_page
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I select Checklists from the Tools menu$')
|
||||
def i_select_checklists(step):
|
||||
|
||||
@@ -59,7 +59,7 @@ class Command(BaseCommand):
|
||||
discussion_items = _get_discussion_items(course)
|
||||
|
||||
# now query all discussion items via get_items() and compare with the tree-traversal
|
||||
queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
|
||||
queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
|
||||
'discussion', None, None])
|
||||
|
||||
for item in queried_discussion_items:
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from auth.authz import _copy_course_group
|
||||
@@ -16,8 +15,7 @@ from auth.authz import _copy_course_group
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Clone a MongoDB backed course to another location'''
|
||||
help = 'Clone a MongoDB backed course to another location'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 2:
|
||||
|
||||
@@ -5,7 +5,6 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from .prompt import query_yes_no
|
||||
|
||||
@@ -38,7 +37,7 @@ class Command(BaseCommand):
|
||||
if query_yes_no("Deleting course {0}. Confirm?".format(loc_str), default="no"):
|
||||
if query_yes_no("Are you sure. This action cannot be undone!", default="no"):
|
||||
loc = CourseDescriptor.id_to_location(loc_str)
|
||||
if delete_course(ms, cs, loc, commit) == True:
|
||||
if delete_course(ms, cs, loc, commit):
|
||||
print 'removing User permissions from course....'
|
||||
# in the django layer, we need to remove all the user permissions groups associated with this course
|
||||
if commit:
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.core.management.base import BaseCommand, CommandError
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
|
||||
@@ -15,8 +14,7 @@ unnamed_modules = 0
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Import the specified data directory into the default ModuleStore'''
|
||||
help = 'Import the specified data directory into the default ModuleStore'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 2:
|
||||
|
||||
@@ -12,8 +12,7 @@ unnamed_modules = 0
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Import the specified data directory into the default ModuleStore'''
|
||||
help = 'Import the specified data directory into the default ModuleStore'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) == 0:
|
||||
@@ -28,4 +27,4 @@ class Command(BaseCommand):
|
||||
data=data_dir,
|
||||
courses=course_dirs)
|
||||
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False,
|
||||
static_content_store=contentstore(), verbose=True)
|
||||
static_content_store=contentstore(), verbose=True)
|
||||
|
||||
@@ -11,8 +11,8 @@ def query_yes_no(question, default="yes"):
|
||||
|
||||
The "answer" return value is one of "yes" or "no".
|
||||
"""
|
||||
valid = {"yes":True, "y":True, "ye":True,
|
||||
"no":False, "n":False}
|
||||
valid = {"yes": True, "y": True, "ye": True,
|
||||
"no": False, "n": False}
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
@@ -30,5 +30,4 @@ def query_yes_no(question, default="yes"):
|
||||
elif choice in valid:
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' "\
|
||||
"(or 'y' or 'n').\n")
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from xmodule.templates import update_templates
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = \
|
||||
'''Imports and updates the Studio component templates from the code pack and put in the DB'''
|
||||
help = 'Imports and updates the Studio component templates from the code pack and put in the DB'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
update_templates()
|
||||
update_templates()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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
|
||||
@@ -9,10 +7,11 @@ 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)]
|
||||
'''
|
||||
'''
|
||||
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 directory> [<course dir>...]")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from static_replace import replace_static_urls
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore import Location
|
||||
from django.http import Http404
|
||||
|
||||
|
||||
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
|
||||
|
||||
@@ -6,13 +6,14 @@ from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
from tempdir import mkdtemp_clean
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
from json import loads
|
||||
import traceback
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.dispatch import Signal
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from .utils import ModuleStoreTestCase, parse_json
|
||||
@@ -37,6 +38,7 @@ TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
|
||||
|
||||
class MongoCollectionFindWrapper(object):
|
||||
def __init__(self, original):
|
||||
self.original = original
|
||||
@@ -92,6 +94,33 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
return cnt
|
||||
|
||||
def test_get_items(self):
|
||||
'''
|
||||
This verifies a bug we had where the None setting in get_items() meant 'wildcard'
|
||||
Unfortunately, None = published for the revision field, so get_items() would return
|
||||
both draft and non-draft copies.
|
||||
'''
|
||||
store = modulestore()
|
||||
draft_store = modulestore('draft')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
|
||||
draft_store.clone_item(html_module.location, html_module.location)
|
||||
|
||||
# now query get_items() to get this location with revision=None, this should just
|
||||
# return back a single item (not 2)
|
||||
|
||||
items = store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertFalse(getattr(items[0], 'is_draft', False))
|
||||
|
||||
# now refetch from the draft store. Note that even though we pass
|
||||
# None in the revision field, the draft store will replace that with 'draft'
|
||||
items = draft_store.get_items(['i4x', 'edX', 'simple', 'html', 'test_html', None])
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertTrue(getattr(items[0], 'is_draft', False))
|
||||
|
||||
def test_draft_metadata(self):
|
||||
'''
|
||||
This verifies a bug we had where inherited metadata was getting written to the
|
||||
@@ -158,32 +187,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_get_depth_with_drafts(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
|
||||
|
||||
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
|
||||
# make sure no draft items have been returned
|
||||
num_drafts = self._get_draft_counts(course)
|
||||
self.assertEqual(num_drafts, 0)
|
||||
|
||||
problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'problem', 'ps01-simple', None]))
|
||||
problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'problem', 'ps01-simple', None]))
|
||||
|
||||
# put into draft
|
||||
modulestore('draft').clone_item(problem.location, problem.location)
|
||||
|
||||
# make sure we can query that item and verify that it is a draft
|
||||
draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'problem', 'ps01-simple', None]))
|
||||
self.assertTrue(getattr(draft_problem,'is_draft', False))
|
||||
draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'problem', 'ps01-simple', None]))
|
||||
self.assertTrue(getattr(draft_problem, 'is_draft', False))
|
||||
|
||||
#now requery with depth
|
||||
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
|
||||
# make sure just one draft item have been returned
|
||||
num_drafts = self._get_draft_counts(course)
|
||||
self.assertEqual(num_drafts, 1)
|
||||
|
||||
self.assertEqual(num_drafts, 1)
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
@@ -215,13 +243,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
module_store = modulestore('direct')
|
||||
found = False
|
||||
|
||||
item = None
|
||||
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
|
||||
found = len(items) > 0
|
||||
|
||||
self.assertTrue(found)
|
||||
# check that there's actually content in the 'question' field
|
||||
self.assertGreater(len(items[0].question),0)
|
||||
self.assertGreater(len(items[0].question), 0)
|
||||
|
||||
def test_xlint_fails(self):
|
||||
err_cnt = perform_xlint('common/test/data', ['full'])
|
||||
@@ -234,14 +261,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
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]))
|
||||
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.children)
|
||||
|
||||
self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
|
||||
"application/json")
|
||||
json.dumps({'id': sequential.location.url(), 'delete_children': 'true',
|
||||
'delete_all_versions': 'true'}),
|
||||
"application/json")
|
||||
|
||||
found = False
|
||||
try:
|
||||
@@ -252,7 +280,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertFalse(found)
|
||||
|
||||
chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', 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.assertFalse(sequential.location.url() in chapter.children)
|
||||
@@ -275,7 +303,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
course = module_store.get_item(source_location)
|
||||
@@ -288,7 +315,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
}
|
||||
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
@@ -347,17 +374,44 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
def test_export_course(self):
|
||||
module_store = modulestore('direct')
|
||||
draft_store = modulestore('draft')
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
|
||||
|
||||
# get a vertical (and components in it) to put into 'draft'
|
||||
vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]), depth=1)
|
||||
|
||||
draft_store.clone_item(vertical.location, vertical.location)
|
||||
|
||||
for child in vertical.get_children():
|
||||
draft_store.clone_item(child.location, child.location)
|
||||
|
||||
root_dir = path(mkdtemp_clean())
|
||||
|
||||
# now create a private vertical
|
||||
private_vertical = draft_store.clone_item(vertical.location,
|
||||
Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None]))
|
||||
|
||||
# add private to list of children
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
private_location_no_draft = private_vertical.location._replace(revision=None)
|
||||
module_store.update_children(sequential.location, sequential.children +
|
||||
[private_location_no_draft.url()])
|
||||
|
||||
# read back the sequential, to make sure we have a pointer to
|
||||
sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'sequential', 'Administrivia_and_Circuit_Elements', None]))
|
||||
|
||||
self.assertIn(private_location_no_draft.url(), sequential.children)
|
||||
|
||||
print 'Exporting to tempdir = {0}'.format(root_dir)
|
||||
|
||||
# export out to a tempdir
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
|
||||
|
||||
# check for static tabs
|
||||
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
|
||||
@@ -391,20 +445,36 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
delete_course(module_store, content_store, location)
|
||||
|
||||
# reimport
|
||||
import_from_xml(module_store, root_dir, ['test_export'])
|
||||
import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store)
|
||||
|
||||
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
|
||||
self.assertGreater(len(items), 0)
|
||||
for descriptor in items:
|
||||
print "Checking {0}....".format(descriptor.location.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# don't try to look at private verticals. Right now we're running
|
||||
# the service in non-draft aware
|
||||
if getattr(descriptor, 'is_draft', False):
|
||||
print "Checking {0}....".format(descriptor.location.url())
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# verify that we have the content in the draft store as well
|
||||
vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]), depth=1)
|
||||
|
||||
self.assertTrue(getattr(vertical, 'is_draft', False))
|
||||
for child in vertical.get_children():
|
||||
self.assertTrue(getattr(child, 'is_draft', False))
|
||||
|
||||
# verify that we have the private vertical
|
||||
test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
|
||||
'vertical', 'vertical_66', None]))
|
||||
|
||||
self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
|
||||
|
||||
shutil.rmtree(root_dir)
|
||||
|
||||
def test_course_handouts_rewrites(self):
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
# import a test course
|
||||
import_from_xml(module_store, 'common/test/data/', ['full'])
|
||||
@@ -437,11 +507,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
# make sure we pre-fetched a known sequential which should be at depth=2
|
||||
self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
|
||||
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
|
||||
'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
|
||||
|
||||
# make sure we don't have a specific vertical which should be at depth=3
|
||||
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
|
||||
None]) in course.system.module_data)
|
||||
self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None])
|
||||
in course.system.module_data)
|
||||
|
||||
def test_export_course_with_unknown_metadata(self):
|
||||
module_store = modulestore('direct')
|
||||
@@ -468,10 +538,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
exported = True
|
||||
except Exception:
|
||||
print 'Exception thrown: {0}'.format(traceback.format_exc())
|
||||
pass
|
||||
|
||||
self.assertTrue(exported)
|
||||
|
||||
|
||||
class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the CMS ContentStore application.
|
||||
@@ -506,7 +578,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
'org': 'MITx',
|
||||
'number': '999',
|
||||
'display_name': 'Robot Super Course',
|
||||
}
|
||||
}
|
||||
|
||||
def test_create_course(self):
|
||||
"""Test new course creation - happy path"""
|
||||
@@ -533,7 +605,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(data['ErrMsg'],
|
||||
'There is already a course defined with the same organization and course number.')
|
||||
'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"""
|
||||
@@ -543,16 +615,16 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
|
||||
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'.")
|
||||
"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
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<h1 class="title-1">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
'<h1 class="title-1">My Courses</h1>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_factory(self):
|
||||
"""Test that the course factory works correctly."""
|
||||
@@ -570,25 +642,25 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
CourseFactory.create(display_name='Robot Super Educational Course')
|
||||
resp = self.client.get(reverse('index'))
|
||||
self.assertContains(resp,
|
||||
'<span class="class-name">Robot Super Educational Course</span>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
'<span class="class-name">Robot Super Educational Course</span>',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_course_overview_view_with_course(self):
|
||||
"""Test viewing the course overview page with an existing course"""
|
||||
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
|
||||
|
||||
data = {
|
||||
'org': 'MITx',
|
||||
'course': '999',
|
||||
'name': Location.clean('Robot Super Course'),
|
||||
}
|
||||
'org': 'MITx',
|
||||
'course': '999',
|
||||
'name': Location.clean('Robot Super Course'),
|
||||
}
|
||||
|
||||
resp = self.client.get(reverse('course_index', kwargs=data))
|
||||
self.assertContains(resp,
|
||||
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
|
||||
status_code=200,
|
||||
html=True)
|
||||
'<article class="courseware-overview" data-course-id="i4x://MITx/999/course/Robot_Super_Course">',
|
||||
status_code=200,
|
||||
html=True)
|
||||
|
||||
def test_clone_item(self):
|
||||
"""Test cloning an item. E.g. creating a new section"""
|
||||
@@ -598,14 +670,13 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
'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)
|
||||
self.assertRegexpMatches(data['id'],
|
||||
'^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
|
||||
self.assertRegexpMatches(data['id'], '^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
|
||||
|
||||
def test_capa_module(self):
|
||||
"""Test that a problem treats markdown specially."""
|
||||
@@ -614,7 +685,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
problem_data = {
|
||||
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
|
||||
'template': 'i4x://edx/templates/problem/Blank_Common_Problem'
|
||||
}
|
||||
}
|
||||
|
||||
resp = self.client.post(reverse('clone_item'), problem_data)
|
||||
|
||||
@@ -748,6 +819,45 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
# make sure we found the item (e.g. it didn't error while loading)
|
||||
self.assertTrue(did_load_item)
|
||||
|
||||
def test_forum_id_generation(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
module_store = modulestore('direct')
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
|
||||
|
||||
# crate a new module and add it as a child to a vertical
|
||||
module_store.clone_item(source_template_location, new_component_location)
|
||||
|
||||
new_discussion_item = module_store.get_item(new_component_location)
|
||||
|
||||
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
|
||||
|
||||
def test_update_modulestore_signal_did_fire(self):
|
||||
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
module_store = modulestore('direct')
|
||||
|
||||
try:
|
||||
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
|
||||
|
||||
self.got_signal = False
|
||||
|
||||
def _signal_hander(modulestore=None, course_id=None, location=None, **kwargs):
|
||||
self.got_signal = True
|
||||
|
||||
module_store.modulestore_update_signal.connect(_signal_hander)
|
||||
|
||||
new_component_location = Location('i4x', 'edX', 'full', 'html', 'new_component')
|
||||
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Blank_HTML_Page')
|
||||
|
||||
# crate a new module
|
||||
module_store.clone_item(source_template_location, new_component_location)
|
||||
|
||||
finally:
|
||||
module_store.modulestore_update_signal = None
|
||||
|
||||
self.assertTrue(self.got_signal)
|
||||
|
||||
def test_metadata_inheritance(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ class CachingTestCase(TestCase):
|
||||
def test_put_and_get(self):
|
||||
set_cached_content(self.mockAsset)
|
||||
self.assertEqual(self.mockAsset.content, get_cached_content(self.unicodeLocation).content,
|
||||
'should be stored in cache with unicodeLocation')
|
||||
'should be stored in cache with unicodeLocation')
|
||||
self.assertEqual(self.mockAsset.content, get_cached_content(self.nonUnicodeLocation).content,
|
||||
'should be stored in cache with nonUnicodeLocation')
|
||||
'should be stored in cache with nonUnicodeLocation')
|
||||
|
||||
def test_delete(self):
|
||||
set_cached_content(self.mockAsset)
|
||||
del_cached_content(self.nonUnicodeLocation)
|
||||
self.assertEqual(None, get_cached_content(self.unicodeLocation),
|
||||
'should not be stored in cache with unicodeLocation')
|
||||
'should not be stored in cache with unicodeLocation')
|
||||
self.assertEqual(None, get_cached_content(self.nonUnicodeLocation),
|
||||
'should not be stored in cache with nonUnicodeLocation')
|
||||
'should not be stored in cache with nonUnicodeLocation')
|
||||
|
||||
@@ -8,8 +8,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import UTC
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from models.settings.course_details import (CourseDetails,
|
||||
CourseSettingsEncoder)
|
||||
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
@@ -21,6 +20,7 @@ from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.fields import Date
|
||||
|
||||
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
@@ -87,16 +87,16 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
jsondetails.syllabus = "<a href='foo'>bar</a>"
|
||||
# encode - decode to convert date fields and other data which changes form
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).syllabus,
|
||||
jsondetails.syllabus, "After set syllabus")
|
||||
jsondetails.syllabus, "After set syllabus")
|
||||
jsondetails.overview = "Overview"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).overview,
|
||||
jsondetails.overview, "After set overview")
|
||||
jsondetails.overview, "After set overview")
|
||||
jsondetails.intro_video = "intro_video"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).intro_video,
|
||||
jsondetails.intro_video, "After set intro_video")
|
||||
jsondetails.intro_video, "After set intro_video")
|
||||
jsondetails.effort = "effort"
|
||||
self.assertEqual(CourseDetails.update_from_json(jsondetails.__dict__).effort,
|
||||
jsondetails.effort, "After set effort")
|
||||
jsondetails.effort, "After set effort")
|
||||
|
||||
|
||||
class CourseDetailsViewTest(CourseTestCase):
|
||||
@@ -151,8 +151,8 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
@staticmethod
|
||||
def struct_to_datetime(struct_time):
|
||||
return datetime.datetime(struct_time.tm_year, struct_time.tm_mon,
|
||||
struct_time.tm_mday, struct_time.tm_hour,
|
||||
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
|
||||
struct_time.tm_mday, struct_time.tm_hour,
|
||||
struct_time.tm_min, struct_time.tm_sec, tzinfo=UTC())
|
||||
|
||||
def compare_date_fields(self, details, encoded, context, field):
|
||||
if details[field] is not None:
|
||||
@@ -249,6 +249,7 @@ class CourseGradingTest(CourseTestCase):
|
||||
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
|
||||
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
|
||||
|
||||
|
||||
class CourseMetadataEditingTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
CourseTestCase.setUp(self)
|
||||
@@ -256,7 +257,6 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
|
||||
|
||||
|
||||
def test_fetch_initial_fields(self):
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
@@ -272,17 +272,17 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
|
||||
def test_update_from_json(self):
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "advertised_start" : "start A",
|
||||
"testcenter_info" : { "c" : "test" },
|
||||
"days_early_for_beta" : 2})
|
||||
{"advertised_start": "start A",
|
||||
"testcenter_info": {"c": "test"},
|
||||
"days_early_for_beta": 2})
|
||||
self.update_check(test_model)
|
||||
# try fresh fetch to ensure persistence
|
||||
test_model = CourseMetadata.fetch(self.course_location)
|
||||
self.update_check(test_model)
|
||||
# now change some of the existing metadata
|
||||
test_model = CourseMetadata.update_from_json(self.course_location,
|
||||
{ "advertised_start" : "start B",
|
||||
"display_name" : "jolly roger"})
|
||||
{"advertised_start": "start B",
|
||||
"display_name": "jolly roger"})
|
||||
self.assertIn('display_name', test_model, 'Missing editable metadata field')
|
||||
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value")
|
||||
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
|
||||
@@ -294,13 +294,12 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
|
||||
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value")
|
||||
self.assertIn('testcenter_info', test_model, 'Missing testcenter_info metadata field')
|
||||
self.assertDictEqual(test_model['testcenter_info'], { "c" : "test" }, "testcenter_info not expected value")
|
||||
self.assertDictEqual(test_model['testcenter_info'], {"c": "test"}, "testcenter_info not expected value")
|
||||
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
|
||||
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value")
|
||||
|
||||
|
||||
def test_delete_key(self):
|
||||
test_model = CourseMetadata.delete_key(self.fullcourse_location, { 'deleteKeys' : ['doesnt_exist', 'showanswer', 'xqa_key']})
|
||||
test_model = CourseMetadata.delete_key(self.fullcourse_location, {'deleteKeys': ['doesnt_exist', 'showanswer', 'xqa_key']})
|
||||
# ensure no harm
|
||||
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
|
||||
self.assertIn('display_name', test_model, 'full missing editable metadata field')
|
||||
|
||||
@@ -69,4 +69,5 @@ class UrlReverseTestCase(ModuleStoreTestCase):
|
||||
self.assertEquals(
|
||||
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
|
||||
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
import json
|
||||
import shutil
|
||||
from django.test.client import Client
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from path import path
|
||||
import json
|
||||
from fs.osfs import OSFS
|
||||
import copy
|
||||
|
||||
from contentstore.utils import get_modulestore
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.store_utilities import clone_course
|
||||
from xmodule.modulestore.store_utilities import delete_course
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.templates import update_templates
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from .utils import ModuleStoreTestCase, parse_json, user, registration
|
||||
|
||||
|
||||
@@ -84,6 +61,7 @@ class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(email).is_active)
|
||||
|
||||
|
||||
class AuthTestCase(ContentStoreTestCase):
|
||||
"""Check that various permissions-related things work"""
|
||||
|
||||
@@ -101,9 +79,9 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
def test_public_pages_load(self):
|
||||
"""Make sure pages that don't require login load without error."""
|
||||
pages = (
|
||||
reverse('login'),
|
||||
reverse('signup'),
|
||||
)
|
||||
reverse('login'),
|
||||
reverse('signup'),
|
||||
)
|
||||
for page in pages:
|
||||
print "Checking '{0}'".format(page)
|
||||
self.check_page_get(page, 200)
|
||||
@@ -136,13 +114,13 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
"""Make sure pages that do require login work."""
|
||||
auth_pages = (
|
||||
reverse('index'),
|
||||
)
|
||||
)
|
||||
|
||||
# These are pages that should just load when the user is logged in
|
||||
# (no data needed)
|
||||
simple_auth_pages = (
|
||||
reverse('index'),
|
||||
)
|
||||
)
|
||||
|
||||
# need an activated user
|
||||
self.test_create_account()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -9,7 +8,8 @@ import copy
|
||||
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
|
||||
|
||||
#In order to instantiate an open ended tab automatically, need to have this data
|
||||
OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"}
|
||||
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
|
||||
|
||||
|
||||
def get_modulestore(location):
|
||||
"""
|
||||
@@ -86,11 +86,10 @@ def get_lms_link_for_item(location, preview=False, course_id=None):
|
||||
|
||||
if settings.LMS_BASE is not None:
|
||||
if preview:
|
||||
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE', 'preview.' + settings.LMS_BASE)
|
||||
else:
|
||||
lms_base = settings.LMS_BASE
|
||||
|
||||
|
||||
lms_link = "//{lms_base}/courses/{course_id}/jump_to/{location}".format(
|
||||
lms_base=lms_base,
|
||||
course_id=course_id,
|
||||
@@ -192,6 +191,7 @@ class CoursePageNames:
|
||||
CourseOutline = "course_index"
|
||||
Checklists = "checklists"
|
||||
|
||||
|
||||
def add_open_ended_panel_tab(course):
|
||||
"""
|
||||
Used to add the open ended panel tab to a course if it does not exist.
|
||||
@@ -208,6 +208,7 @@ def add_open_ended_panel_tab(course):
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
|
||||
|
||||
def remove_open_ended_panel_tab(course):
|
||||
"""
|
||||
Used to remove the open ended panel tab from a course if it exists.
|
||||
@@ -220,6 +221,6 @@ def remove_open_ended_panel_tab(course):
|
||||
#Check to see if open ended panel is defined in the course
|
||||
if OPEN_ENDED_PANEL in course_tabs:
|
||||
#Add panel to the tabs if it is not defined
|
||||
course_tabs = [ct for ct in course_tabs if ct!=OPEN_ENDED_PANEL]
|
||||
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
|
||||
@@ -14,9 +14,6 @@ from tempfile import mkdtemp
|
||||
from django.core.servers.basehttp import FileWrapper
|
||||
from django.core.files.temp import NamedTemporaryFile
|
||||
|
||||
# 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, HttpResponseServerError
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -244,8 +241,7 @@ def edit_subsection(request, location):
|
||||
(field.name, field.read_from(item))
|
||||
for field
|
||||
in item.fields
|
||||
if field.name not in ['display_name', 'start', 'due', 'format'] and
|
||||
field.scope == Scope.settings
|
||||
if field.name not in ['display_name', 'start', 'due', 'format'] and field.scope == Scope.settings
|
||||
)
|
||||
|
||||
can_view_live = False
|
||||
@@ -257,18 +253,18 @@ def edit_subsection(request, location):
|
||||
break
|
||||
|
||||
return render_to_response('edit_subsection.html',
|
||||
{'subsection': item,
|
||||
'context_course': course,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'parent_item': parent,
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
})
|
||||
{'subsection': item,
|
||||
'context_course': course,
|
||||
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
|
||||
'lms_link': lms_link,
|
||||
'preview_link': preview_link,
|
||||
'course_graders': json.dumps(CourseGradingModel.fetch(course.location).graders),
|
||||
'parent_location': course.location,
|
||||
'parent_item': parent,
|
||||
'policy_metadata': policy_metadata,
|
||||
'subsection_units': subsection_units,
|
||||
'can_view_live': can_view_live
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -347,17 +343,17 @@ def edit_unit(request, location):
|
||||
index = index + 1
|
||||
|
||||
preview_lms_base = settings.MITX_FEATURES.get('PREVIEW_LMS_BASE',
|
||||
'preview.' + settings.LMS_BASE)
|
||||
'preview.' + settings.LMS_BASE)
|
||||
|
||||
preview_lms_link = '//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview_lms_base=preview_lms_base,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index)
|
||||
preview_lms_base=preview_lms_base,
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index)
|
||||
|
||||
unit_state = compute_unit_state(item)
|
||||
|
||||
@@ -623,7 +619,6 @@ def delete_item(request):
|
||||
|
||||
store = get_modulestore(item_loc)
|
||||
|
||||
|
||||
# @TODO: this probably leaves draft items dangling. My preferance would be for the semantic to be
|
||||
# if item.location.revision=None, then delete both draft and published version
|
||||
# if caller wants to only delete the draft than the caller should put item.location.revision='draft'
|
||||
@@ -665,7 +660,7 @@ def save_item(request):
|
||||
if not has_access(request.user, item_location):
|
||||
raise PermissionDenied()
|
||||
|
||||
store = get_modulestore(Location(item_location));
|
||||
store = get_modulestore(Location(item_location))
|
||||
|
||||
if request.POST.get('data') is not None:
|
||||
data = request.POST['data']
|
||||
@@ -800,7 +795,7 @@ def upload_asset(request, org, course, coursename):
|
||||
# Does the course actually exist?!? Get anything from it to prove its existance
|
||||
|
||||
try:
|
||||
item = modulestore().get_item(location)
|
||||
modulestore().get_item(location)
|
||||
except:
|
||||
# no return it as a Bad Request response
|
||||
logging.error('Could not find course' + location)
|
||||
@@ -834,24 +829,23 @@ def upload_asset(request, org, course, coursename):
|
||||
readback = contentstore().find(content.location)
|
||||
|
||||
response_payload = {'displayname': content.name,
|
||||
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
|
||||
'url': StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||
'msg': 'Upload completed'
|
||||
}
|
||||
'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
|
||||
'url': StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
|
||||
'msg': 'Upload completed'
|
||||
}
|
||||
|
||||
response = HttpResponse(json.dumps(response_payload))
|
||||
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
|
||||
return response
|
||||
|
||||
|
||||
'''
|
||||
This view will return all CMS users who are editors for the specified course
|
||||
'''
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def manage_users(request, location):
|
||||
|
||||
'''
|
||||
This view will return all CMS users who are editors for the specified course
|
||||
'''
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=STAFF_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
@@ -878,14 +872,14 @@ def create_json_response(errmsg=None):
|
||||
return resp
|
||||
|
||||
|
||||
'''
|
||||
This POST-back view will add a user - specified by email - to the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def add_user(request, location):
|
||||
'''
|
||||
This POST-back view will add a user - specified by email - to the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
email = request.POST["email"]
|
||||
|
||||
if email == '':
|
||||
@@ -911,14 +905,15 @@ def add_user(request, location):
|
||||
return create_json_response()
|
||||
|
||||
|
||||
'''
|
||||
This POST-back view will remove a user - specified by email - from the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
@expect_json
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def remove_user(request, location):
|
||||
'''
|
||||
This POST-back view will remove a user - specified by email - from the list of editors for
|
||||
the specified course
|
||||
'''
|
||||
|
||||
email = request.POST["email"]
|
||||
|
||||
# check that logged in user has admin permissions on this course
|
||||
@@ -993,13 +988,12 @@ def reorder_static_tabs(request):
|
||||
for tab in course.tabs:
|
||||
if tab['type'] == 'static_tab':
|
||||
reordered_tabs.append({'type': 'static_tab',
|
||||
'name': tab_items[static_tab_idx].display_name,
|
||||
'url_slug': tab_items[static_tab_idx].location.name})
|
||||
'name': tab_items[static_tab_idx].display_name,
|
||||
'url_slug': tab_items[static_tab_idx].location.name})
|
||||
static_tab_idx += 1
|
||||
else:
|
||||
reordered_tabs.append(tab)
|
||||
|
||||
|
||||
# OK, re-assemble the static tabs in the new order
|
||||
course.tabs = reordered_tabs
|
||||
modulestore('direct').update_metadata(course.location, own_metadata(course))
|
||||
@@ -1011,7 +1005,6 @@ def reorder_static_tabs(request):
|
||||
def edit_tabs(request, org, course, coursename):
|
||||
location = ['i4x', org, course, 'course', coursename]
|
||||
course_item = modulestore().get_item(location)
|
||||
static_tabs_loc = Location('i4x', org, course, 'static_tab', None)
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location):
|
||||
@@ -1040,7 +1033,7 @@ def edit_tabs(request, org, course, coursename):
|
||||
'active_tab': 'pages',
|
||||
'context_course': course_item,
|
||||
'components': components
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
def not_found(request):
|
||||
@@ -1102,21 +1095,21 @@ def course_info_updates(request, org, course, provided_id=None):
|
||||
|
||||
if request.method == 'GET':
|
||||
return HttpResponse(json.dumps(get_course_updates(location)),
|
||||
mimetype="application/json")
|
||||
mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
try:
|
||||
return HttpResponse(json.dumps(delete_course_update(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to delete",
|
||||
content_type="text/plain")
|
||||
content_type="text/plain")
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
return HttpResponse(json.dumps(update_course_updates(location,
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
request.POST, provided_id)), mimetype="application/json")
|
||||
except:
|
||||
return HttpResponseBadRequest("Failed to save",
|
||||
content_type="text/plain")
|
||||
content_type="text/plain")
|
||||
|
||||
|
||||
@expect_json
|
||||
@@ -1184,7 +1177,7 @@ def course_config_graders_page(request, org, course, name):
|
||||
|
||||
return render_to_response('settings_graders.html', {
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'course_location': location,
|
||||
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder)
|
||||
})
|
||||
|
||||
@@ -1203,8 +1196,8 @@ def course_config_advanced_page(request, org, course, name):
|
||||
|
||||
return render_to_response('settings_advanced.html', {
|
||||
'context_course': course_module,
|
||||
'course_location' : location,
|
||||
'advanced_dict' : json.dumps(CourseMetadata.fetch(location)),
|
||||
'course_location': location,
|
||||
'advanced_dict': json.dumps(CourseMetadata.fetch(location)),
|
||||
})
|
||||
|
||||
|
||||
@@ -1225,7 +1218,8 @@ def course_settings_updates(request, org, course, name, section):
|
||||
manager = CourseDetails
|
||||
elif section == 'grading':
|
||||
manager = CourseGradingModel
|
||||
else: return
|
||||
else:
|
||||
return
|
||||
|
||||
if request.method == 'GET':
|
||||
# Cannot just do a get w/o knowing the course name :-(
|
||||
@@ -1320,6 +1314,7 @@ def course_advanced_updates(request, org, course, name):
|
||||
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
|
||||
return HttpResponse(response_json, mimetype="application/json")
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def get_checklists(request, org, course, name):
|
||||
@@ -1345,10 +1340,10 @@ def get_checklists(request, org, course, name):
|
||||
if copied or modified:
|
||||
modulestore.update_metadata(location, own_metadata(course_module))
|
||||
return render_to_response('checklists.html',
|
||||
{
|
||||
'context_course': course_module,
|
||||
'checklists': checklists
|
||||
})
|
||||
{
|
||||
'context_course': course_module,
|
||||
'checklists': checklists
|
||||
})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -1433,7 +1428,6 @@ def asset_index(request, org, course, name):
|
||||
# sort in reverse upload date order
|
||||
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
|
||||
|
||||
thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference)
|
||||
asset_display = []
|
||||
for asset in assets:
|
||||
id = asset['_id']
|
||||
@@ -1527,10 +1521,10 @@ def initialize_course_tabs(course):
|
||||
# This logic is repeated in xmodule/modulestore/tests/factories.py
|
||||
# so if you change anything here, you need to also change it there.
|
||||
course.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"},
|
||||
{"type": "progress", "name": "Progress"}]
|
||||
|
||||
modulestore('direct').update_metadata(course.location.url(), own_metadata(course))
|
||||
|
||||
@@ -1586,7 +1580,10 @@ def import_course(request, org, course, name):
|
||||
shutil.move(r / fname, course_dir)
|
||||
|
||||
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
||||
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
|
||||
[course_subdir], load_error_modules=False,
|
||||
static_content_store=contentstore(),
|
||||
target_location_namespace=Location(location),
|
||||
draft_store=modulestore())
|
||||
|
||||
# we can blow this away when we're done importing.
|
||||
shutil.rmtree(course_dir)
|
||||
@@ -1620,8 +1617,8 @@ def generate_export_course(request, org, course, name):
|
||||
|
||||
logging.debug('root = {0}'.format(root_dir))
|
||||
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
|
||||
# filename = root_dir / name + '.tar.gz'
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
|
||||
#filename = root_dir / name + '.tar.gz'
|
||||
|
||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||
tf = tarfile.open(name=export_file.name, mode='w:gz')
|
||||
|
||||
@@ -174,7 +174,6 @@ class CourseDetails(object):
|
||||
return result
|
||||
|
||||
|
||||
|
||||
# TODO move to a more general util? Is there a better way to do the isinstance model check?
|
||||
class CourseSettingsEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
|
||||
@@ -45,14 +45,13 @@ class CourseGradingModel(object):
|
||||
|
||||
# return empty model
|
||||
else:
|
||||
return {
|
||||
"id": index,
|
||||
return {"id": index,
|
||||
"type": "",
|
||||
"min_count": 0,
|
||||
"drop_count": 0,
|
||||
"short_label": None,
|
||||
"weight": 0
|
||||
}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fetch_cutoffs(course_location):
|
||||
@@ -95,7 +94,6 @@ class CourseGradingModel(object):
|
||||
|
||||
return CourseGradingModel.fetch(course_location)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grader_from_json(course_location, grader):
|
||||
"""
|
||||
@@ -137,7 +135,6 @@ class CourseGradingModel(object):
|
||||
|
||||
return cutoffs
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_grace_period_from_json(course_location, graceperiodjson):
|
||||
"""
|
||||
@@ -210,8 +207,7 @@ class CourseGradingModel(object):
|
||||
location = Location(location)
|
||||
|
||||
descriptor = get_modulestore(location).get_item(location)
|
||||
return {
|
||||
"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
|
||||
return {"graderType": descriptor.lms.format if descriptor.lms.format is not None else 'Not Graded',
|
||||
"location": location,
|
||||
"id": 99 # just an arbitrary value to
|
||||
}
|
||||
@@ -231,7 +227,6 @@ class CourseGradingModel(object):
|
||||
|
||||
get_modulestore(location).update_metadata(location, descriptor._model_data._kvs._metadata)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def convert_set_grace_period(descriptor):
|
||||
# 5 hours 59 minutes 59 seconds => converted to iso format
|
||||
@@ -262,13 +257,12 @@ class CourseGradingModel(object):
|
||||
@staticmethod
|
||||
def parse_grader(json_grader):
|
||||
# manual to clear out kruft
|
||||
result = {
|
||||
"type": json_grader["type"],
|
||||
"min_count": int(json_grader.get('min_count', 0)),
|
||||
"drop_count": int(json_grader.get('drop_count', 0)),
|
||||
"short_label": json_grader.get('short_label', None),
|
||||
"weight": float(json_grader.get('weight', 0)) / 100.0
|
||||
}
|
||||
result = {"type": json_grader["type"],
|
||||
"min_count": int(json_grader.get('min_count', 0)),
|
||||
"drop_count": int(json_grader.get('drop_count', 0)),
|
||||
"short_label": json_grader.get('short_label', None),
|
||||
"weight": float(json_grader.get('weight', 0)) / 100.0
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from xblock.core import Scope
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
import copy
|
||||
|
||||
|
||||
class CourseMetadata(object):
|
||||
'''
|
||||
For CRUD operations on metadata fields which do not have specific editors
|
||||
@@ -13,8 +14,13 @@ class CourseMetadata(object):
|
||||
The objects have no predefined attrs but instead are obj encodings of the
|
||||
editable metadata.
|
||||
'''
|
||||
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start', 'end',
|
||||
'enrollment_start', 'enrollment_end', 'tabs', 'graceperiod', 'checklists']
|
||||
FILTERED_LIST = XModuleDescriptor.system_metadata_fields + ['start',
|
||||
'end',
|
||||
'enrollment_start',
|
||||
'enrollment_end',
|
||||
'tabs',
|
||||
'graceperiod',
|
||||
'checklists']
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_location):
|
||||
@@ -48,7 +54,7 @@ class CourseMetadata(object):
|
||||
descriptor = get_modulestore(course_location).get_item(course_location)
|
||||
|
||||
dirty = False
|
||||
|
||||
|
||||
#Copy the filtered list to avoid permanently changing the class attribute
|
||||
filtered_list = copy.copy(cls.FILTERED_LIST)
|
||||
#Don't filter on the tab attribute if filter_tabs is False
|
||||
@@ -71,7 +77,7 @@ class CourseMetadata(object):
|
||||
|
||||
if dirty:
|
||||
get_modulestore(course_location).update_metadata(course_location,
|
||||
own_metadata(descriptor))
|
||||
own_metadata(descriptor))
|
||||
|
||||
# Could just generate and return a course obj w/o doing any db reads,
|
||||
# but I put the reads in as a means to confirm it persisted correctly
|
||||
@@ -92,6 +98,6 @@ class CourseMetadata(object):
|
||||
delattr(descriptor.lms, key)
|
||||
|
||||
get_modulestore(course_location).update_metadata(course_location,
|
||||
own_metadata(descriptor))
|
||||
own_metadata(descriptor))
|
||||
|
||||
return cls.fetch(course_location)
|
||||
|
||||
@@ -67,4 +67,4 @@ MODULESTORE = AUTH_TOKENS['MODULESTORE']
|
||||
CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
|
||||
|
||||
# Datadog for events!
|
||||
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
|
||||
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
|
||||
|
||||
@@ -167,7 +167,7 @@ STATICFILES_DIRS = [
|
||||
PROJECT_ROOT / "static",
|
||||
|
||||
# This is how you would use the textbook images locally
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
# ("book", ENV_ROOT / "book_images")
|
||||
]
|
||||
|
||||
# Locale/Internationalization
|
||||
|
||||
@@ -121,3 +121,7 @@ PASSWORD_HASHERS = (
|
||||
|
||||
# dummy segment-io key
|
||||
SEGMENT_IO_KEY = '***REMOVED***'
|
||||
|
||||
# disable NPS survey in test mode
|
||||
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from dogapi import dog_http_api, dog_stats_api
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from django.dispatch import Signal
|
||||
from request_cache.middleware import RequestCache
|
||||
|
||||
from django.core.cache import get_cache, InvalidCacheBackendError
|
||||
from django.core.cache import get_cache
|
||||
|
||||
cache = get_cache('mongo_metadata_inheritance')
|
||||
for store_name in settings.MODULESTORE:
|
||||
@@ -11,6 +12,8 @@ for store_name in settings.MODULESTORE:
|
||||
store.metadata_inheritance_cache_subsystem = cache
|
||||
store.request_cache = RequestCache.get_request_cache()
|
||||
|
||||
modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
|
||||
store.modulestore_update_signal = modulestore_update_signal
|
||||
if hasattr(settings, 'DATADOG_API'):
|
||||
dog_http_api.api_key = settings.DATADOG_API
|
||||
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
|
||||
|
||||
@@ -225,7 +225,6 @@ function toggleSections(e) {
|
||||
function editSectionPublishDate(e) {
|
||||
e.preventDefault();
|
||||
$modal = $('.edit-subsection-publish-settings').show();
|
||||
$modal = $('.edit-subsection-publish-settings').show();
|
||||
$modal.attr('data-id', $(this).attr('data-id'));
|
||||
$modal.find('.start-date').val($(this).attr('data-date'));
|
||||
$modal.find('.start-time').val($(this).attr('data-time'));
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
color: $blue;
|
||||
|
||||
&:hover, &:active {
|
||||
background: $blue-l3;
|
||||
background: $blue-l4;
|
||||
color: $blue-s2;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ input[type="password"],
|
||||
textarea.text {
|
||||
padding: 6px 8px 8px;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid $mediumGrey;
|
||||
border: 1px solid $gray-l2;
|
||||
border-radius: 2px;
|
||||
@include linear-gradient($lightGrey, tint($lightGrey, 90%));
|
||||
background-color: $lightGrey;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
|
||||
@include linear-gradient($gray-l5, $white);
|
||||
background-color: $gray-l5;
|
||||
@include box-shadow(inset 0 1px 2px $shadow-l1);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 11px;
|
||||
color: $baseFontColor;
|
||||
@@ -21,7 +21,7 @@ textarea.text {
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
color: #979faf;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@@ -30,7 +30,72 @@ textarea.text {
|
||||
}
|
||||
}
|
||||
|
||||
// forms - specific
|
||||
// ====================
|
||||
|
||||
// forms - fields - not editable
|
||||
.field.is-not-editable {
|
||||
|
||||
& label.is-focused {
|
||||
color: $gray-d2;
|
||||
}
|
||||
|
||||
label, input, textarea {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// field with error
|
||||
.field.error {
|
||||
|
||||
input, textarea {
|
||||
border-color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// forms - additional UI
|
||||
form {
|
||||
|
||||
.note {
|
||||
@include box-sizing(border-box);
|
||||
|
||||
.title {
|
||||
|
||||
}
|
||||
|
||||
.copy {
|
||||
|
||||
}
|
||||
|
||||
// note with actions
|
||||
&.has-actions {
|
||||
@include clearfix();
|
||||
|
||||
.title {
|
||||
|
||||
}
|
||||
|
||||
.copy {
|
||||
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.note-promotion {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// forms - grandfathered
|
||||
input.search {
|
||||
padding: 6px 15px 8px 30px;
|
||||
@include box-sizing(border-box);
|
||||
@@ -73,4 +138,4 @@ code {
|
||||
background-color: #edf1f5;
|
||||
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
|
||||
font-family: Monaco, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
body.signup, body.signin {
|
||||
|
||||
.wrapper-content {
|
||||
margin: 0;
|
||||
margin: ($baseline*1.5) 0 0 0;
|
||||
padding: 0 $baseline;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -18,7 +18,7 @@ body.signup, body.signin {
|
||||
width: flex-grid(12);
|
||||
margin: 0 auto;
|
||||
color: $gray-d2;
|
||||
|
||||
|
||||
header {
|
||||
position: relative;
|
||||
margin-bottom: $baseline;
|
||||
@@ -121,7 +121,7 @@ body.signup, body.signin {
|
||||
@include font-size(16);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: ($baseline/2);
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
width: 100%;
|
||||
@@ -136,15 +136,15 @@ body.signup, body.signin {
|
||||
}
|
||||
|
||||
:-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: $gray-l3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: $gray-l3;
|
||||
:-ms-input-placeholder {
|
||||
color: $gray-l3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -147,7 +147,7 @@ body.course.settings {
|
||||
}
|
||||
|
||||
label {
|
||||
@include font-size(14);
|
||||
@extend .t-copy-sub1;
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
font-weight: 400;
|
||||
@@ -161,7 +161,7 @@ body.course.settings {
|
||||
@include placeholder($gray-l4);
|
||||
@include font-size(16);
|
||||
@include size(100%,100%);
|
||||
padding: ($baseline/2);
|
||||
padding: ($baseline/2);
|
||||
|
||||
&.long {
|
||||
}
|
||||
@@ -212,7 +212,7 @@ body.course.settings {
|
||||
padding: $baseline;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: $baseline;
|
||||
padding-bottom: $baseline;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -238,33 +238,36 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
|
||||
// not editable fields
|
||||
.field.is-not-editable {
|
||||
|
||||
& label.is-focused {
|
||||
color: $gray-d2;
|
||||
}
|
||||
}
|
||||
|
||||
// field with error
|
||||
.field.error {
|
||||
|
||||
input, textarea {
|
||||
border-color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - basic
|
||||
&.basic {
|
||||
|
||||
.list-input {
|
||||
@include clearfix();
|
||||
padding: 0 ($baseline/2);
|
||||
|
||||
.field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// course details that should appear more like content than elements to change
|
||||
.field.is-not-editable {
|
||||
|
||||
label {
|
||||
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
@extend .t-copy-lead1;
|
||||
@include box-shadow(none);
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
#field-course-organization {
|
||||
float: left;
|
||||
width: flex-grid(2, 9);
|
||||
@@ -281,6 +284,58 @@ body.course.settings {
|
||||
float: left;
|
||||
width: flex-grid(5, 9);
|
||||
}
|
||||
|
||||
// course link note
|
||||
.note-promotion-courseURL {
|
||||
@include box-shadow(0 2px 1px $shadow-l1);
|
||||
@include border-radius(($baseline/5));
|
||||
margin-top: ($baseline*1.5);
|
||||
border: 1px solid $gray-l2;
|
||||
padding: ($baseline/2) 0 0 0;
|
||||
|
||||
.title {
|
||||
@extend .t-copy-sub1;
|
||||
margin: 0 0 ($baseline/10) 0;
|
||||
padding: 0 ($baseline/2);
|
||||
|
||||
.tip {
|
||||
display: inline;
|
||||
margin-left: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
.copy {
|
||||
padding: 0 ($baseline/2) ($baseline/2) ($baseline/2);
|
||||
|
||||
.link-courseURL {
|
||||
@extend .t-copy-lead1;
|
||||
|
||||
&:hover {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-actions {
|
||||
@include box-shadow(inset 0 1px 1px $shadow-l1);
|
||||
border-top: 1px solid $gray-l2;
|
||||
padding: ($baseline/2);
|
||||
background: $gray-l5;
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(13);
|
||||
font-weight: 600;
|
||||
|
||||
.icon {
|
||||
@extend .t-icon;
|
||||
@include font-size(16);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - schedule
|
||||
@@ -322,7 +377,7 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// specific fields - overview
|
||||
#field-course-overview {
|
||||
|
||||
@@ -468,7 +523,7 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.grade-specific-bar {
|
||||
height: 50px !important;
|
||||
}
|
||||
@@ -479,7 +534,7 @@ body.course.settings {
|
||||
li {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 50px;
|
||||
height: 50px;
|
||||
text-align: right;
|
||||
@include border-radius(2px);
|
||||
|
||||
@@ -600,8 +655,8 @@ body.course.settings {
|
||||
}
|
||||
|
||||
#field-course-grading-assignment-shortname,
|
||||
#field-course-grading-assignment-totalassignments,
|
||||
#field-course-grading-assignment-gradeweight,
|
||||
#field-course-grading-assignment-totalassignments,
|
||||
#field-course-grading-assignment-gradeweight,
|
||||
#field-course-grading-assignment-droppable {
|
||||
width: flex-grid(2, 6);
|
||||
}
|
||||
@@ -734,4 +789,4 @@ body.course.settings {
|
||||
.content-supplementary {
|
||||
width: flex-grid(3, 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
from contentstore import utils
|
||||
from contentstore import utils
|
||||
%>
|
||||
|
||||
|
||||
@@ -13,17 +13,17 @@ from contentstore import utils
|
||||
<script src="${static.url('js/vendor/timepicker/jquery.timepicker.js')}"></script>
|
||||
<script src="${static.url('js/vendor/timepicker/datepair.js')}"></script>
|
||||
<script src="${static.url('js/vendor/date.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/template_loader.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/server_error.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/validating_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/settings/main_settings_view.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/models/settings/course_details.js')}"></script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
|
||||
|
||||
// hilighting labels when fields are focused in
|
||||
$("form :input").focus(function() {
|
||||
$("label[for='" + this.id + "']").addClass("is-focused");
|
||||
@@ -32,18 +32,18 @@ from contentstore import utils
|
||||
});
|
||||
var model = new CMS.Models.Settings.CourseDetails();
|
||||
model.urlRoot = '${details_url}';
|
||||
model.fetch({success :
|
||||
model.fetch({success :
|
||||
function(model) {
|
||||
var editor = new CMS.Views.Settings.Details({
|
||||
el: $('.settings-details'),
|
||||
model: model
|
||||
});
|
||||
|
||||
|
||||
editor.render();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -62,10 +62,10 @@ from contentstore import utils
|
||||
<article class="content-primary" role="main">
|
||||
<form id="settings_details" class="settings-details" method="post" action="">
|
||||
<section class="group-settings basic">
|
||||
<header>
|
||||
<header>
|
||||
<h2 class="title-2">Basic Information</h2>
|
||||
<span class="tip">The nuts and bolts of your course</span>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text is-not-editable" id="field-course-organization">
|
||||
@@ -83,45 +83,57 @@ from contentstore import utils
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
</li>
|
||||
</ol>
|
||||
<span class="tip tip-stacked">These are used in <a rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />your course URL</a>, and cannot be changed</span>
|
||||
|
||||
<div class="note note-promotion note-promotion-courseURL has-actions">
|
||||
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
|
||||
<div class="copy">
|
||||
<p><a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}" />${utils.get_lms_link_for_about_page(course_location)}</a></p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a title="Send a note to students via email" href="mailto:john.doe@gmail.com?Subject=Enroll%20in%20COURSENAME&body=Hi,%20COURSENAME,%20provided%20by%20edX,%20is%20almost%20ready%20to%20begin.%20Please%20enroll%20for%20this%20course%20at%20${utils.get_lms_link_for_about_page(course_location)}." class="action action-primary"><i class="ss-icon icon ss-symbolicons-standard icon icon-inline icon-announcement">✉</i> Send an invitation to your students</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings schedule">
|
||||
<header>
|
||||
<header>
|
||||
<h2 class="title-2">Course Schedule</h2>
|
||||
<span class="tip">Important steps and segments of your course</span>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-course-start" id="course-start">
|
||||
<div class="field date" id="field-course-start-date">
|
||||
<label for="course-start-date">Course Start Date</label>
|
||||
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day the course begins</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">First day the course begins</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-start-time">
|
||||
<label for="course-start-time">Course Start Time</label>
|
||||
<input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="field-group field-group-course-end" id="course-end">
|
||||
<div class="field date" id="field-course-end-date">
|
||||
<label for="course-end-date">Course End Date</label>
|
||||
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day your course is active</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">Last day your course is active</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-end-time">
|
||||
<label for="course-end-time">Course End Time</label>
|
||||
<input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<ol class="list-input">
|
||||
@@ -129,33 +141,33 @@ from contentstore import utils
|
||||
<div class="field date" id="field-enrollment-start-date">
|
||||
<label for="course-enrollment-start-date">Enrollment Start Date</label>
|
||||
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day students can enroll</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">First day students can enroll</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-start-time">
|
||||
<label for="course-enrollment-start-time">Enrollment Start Time</label>
|
||||
<input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="field-group field-group-enrollment-end" id="enrollment-end">
|
||||
<div class="field date" id="field-enrollment-end-date">
|
||||
<label for="course-enrollment-end-date">Enrollment End Date</label>
|
||||
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day students can enroll</span>
|
||||
</div>
|
||||
<span class="tip tip-stacked">Last day students can enroll</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-end-time">
|
||||
<label for="course-enrollment-end-time">Enrollment End Time</label>
|
||||
<input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings marketing">
|
||||
<header>
|
||||
@@ -167,45 +179,44 @@ from contentstore import utils
|
||||
<li class="field text" id="field-course-overview">
|
||||
<label for="course-overview">Course Overview</label>
|
||||
<textarea class="tinymce text-editor" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a></span>
|
||||
</li>
|
||||
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
<label for="course-overview">Course Introduction Video</label>
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Current Video</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<div class="input">
|
||||
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Enter your YouTube video's ID (along with any restriction parameters)</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings requirements">
|
||||
<header>
|
||||
<header>
|
||||
<h2 class="title-2">Requirements</h2>
|
||||
<span class="tip">Expectations of the students taking this course</span>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-effort">
|
||||
<label for="course-effort">Hours of Effort per Week</label>
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
|
||||
<span class="tip tip-inline">Time spent on all course work</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<span class="tip tip-inline">Time spent on all course work</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
@@ -215,7 +226,7 @@ from contentstore import utils
|
||||
<p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p>
|
||||
|
||||
<p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
@@ -234,4 +245,4 @@ from contentstore import utils
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
</%block>
|
||||
|
||||
1
cms/templates/widgets/metadata-only-edit.html
Normal file
1
cms/templates/widgets/metadata-only-edit.html
Normal file
@@ -0,0 +1 @@
|
||||
<%include file="metadata-edit.html" />
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<form id="hls-form" enctype="multipart/form-data">
|
||||
<section class="source-edit">
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${metadata['source_code']|h}</textarea>
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${editable_metadata_fields['source_code']|h}</textarea>
|
||||
</section>
|
||||
<div class="submit">
|
||||
<button type="reset" class="hls-compile">Save & Compile to edX XML</button>
|
||||
|
||||
@@ -150,8 +150,8 @@ class InputTypeBase(object):
|
||||
## we can swap this around in the future if there's a more logical
|
||||
## order.
|
||||
|
||||
self.id = state.get('id', xml.get('id'))
|
||||
if self.id is None:
|
||||
self.input_id = state.get('id', xml.get('id'))
|
||||
if self.input_id is None:
|
||||
raise ValueError("input id state is None. xml is {0}".format(
|
||||
etree.tostring(xml)))
|
||||
|
||||
@@ -249,7 +249,7 @@ class InputTypeBase(object):
|
||||
and don't need to override this method.
|
||||
"""
|
||||
context = {
|
||||
'id': self.id,
|
||||
'id': self.input_id,
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'msg': self.msg,
|
||||
@@ -457,8 +457,21 @@ class TextLine(InputTypeBase):
|
||||
"""
|
||||
A text line input. Can do math preview if "math"="1" is specified.
|
||||
|
||||
If the hidden attribute is specified, the textline is hidden and the input id is stored in a div with name equal
|
||||
to the value of the hidden attribute. This is used e.g. for embedding simulations turned into questions.
|
||||
If "trailing_text" is set to a value, then the textline will be shown with
|
||||
the value after the text input, and before the checkmark or any input-specific
|
||||
feedback. HTML will not work, but properly escaped HTML characters will. This
|
||||
feature is useful if you would like to specify a specific type of units for the
|
||||
text input.
|
||||
|
||||
If the hidden attribute is specified, the textline is hidden and the input id
|
||||
is stored in a div with name equal to the value of the hidden attribute. This
|
||||
is used e.g. for embedding simulations turned into questions.
|
||||
|
||||
Example:
|
||||
<texline math="1" trailing_text="m/s" />
|
||||
|
||||
This example will render out a text line with a math preview and the text 'm/s'
|
||||
after the end of the text line.
|
||||
"""
|
||||
|
||||
template = "textline.html"
|
||||
@@ -483,6 +496,7 @@ class TextLine(InputTypeBase):
|
||||
Attribute('dojs', None, render=False),
|
||||
Attribute('preprocessorClassName', None, render=False),
|
||||
Attribute('preprocessorSrc', None, render=False),
|
||||
Attribute('trailing_text', ''),
|
||||
]
|
||||
|
||||
def setup(self):
|
||||
@@ -609,7 +623,6 @@ class CodeInput(InputTypeBase):
|
||||
self.queue_len = self.msg
|
||||
self.msg = self.submitted_msg
|
||||
|
||||
|
||||
def setup(self):
|
||||
''' setup this input type '''
|
||||
self.setup_code_response_rendering()
|
||||
@@ -641,7 +654,7 @@ class MatlabInput(CodeInput):
|
||||
tags = ['matlabinput']
|
||||
|
||||
plot_submitted_msg = ("Submitted. As soon as a response is returned, "
|
||||
"this message will be replaced by that feedback.")
|
||||
"this message will be replaced by that feedback.")
|
||||
|
||||
def setup(self):
|
||||
'''
|
||||
@@ -662,16 +675,16 @@ class MatlabInput(CodeInput):
|
||||
self.queue_len = 1
|
||||
self.msg = self.plot_submitted_msg
|
||||
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
'''
|
||||
Handle AJAX calls directed to this input
|
||||
|
||||
Args:
|
||||
- dispatch (str) - indicates how we want this ajax call to be handled
|
||||
- get (dict) - dictionary of key-value pairs that contain useful data
|
||||
Returns:
|
||||
|
||||
dict - 'success' - whether or not we successfully queued this submission
|
||||
- 'message' - message to be rendered in case of error
|
||||
'''
|
||||
|
||||
if dispatch == 'plot':
|
||||
@@ -679,7 +692,7 @@ class MatlabInput(CodeInput):
|
||||
return {}
|
||||
|
||||
def ungraded_response(self, queue_msg, queuekey):
|
||||
'''
|
||||
'''
|
||||
Handle the response from the XQueue
|
||||
Stores the response in the input_state so it can be rendered later
|
||||
|
||||
@@ -691,7 +704,7 @@ class MatlabInput(CodeInput):
|
||||
nothing
|
||||
'''
|
||||
# check the queuekey against the saved queuekey
|
||||
if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued'
|
||||
if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued'
|
||||
and self.input_state['queuekey'] == queuekey):
|
||||
msg = self._parse_data(queue_msg)
|
||||
# save the queue message so that it can be rendered later
|
||||
@@ -702,9 +715,9 @@ class MatlabInput(CodeInput):
|
||||
def _extra_context(self):
|
||||
''' Set up additional context variables'''
|
||||
extra_context = {
|
||||
'queue_len': str(self.queue_len),
|
||||
'queue_msg': self.queue_msg
|
||||
}
|
||||
'queue_len': str(self.queue_len),
|
||||
'queue_msg': self.queue_msg
|
||||
}
|
||||
return extra_context
|
||||
|
||||
def _parse_data(self, queue_msg):
|
||||
@@ -719,20 +732,19 @@ class MatlabInput(CodeInput):
|
||||
result = json.loads(queue_msg)
|
||||
except (TypeError, ValueError):
|
||||
log.error("External message should be a JSON serialized dict."
|
||||
" Received queue_msg = %s" % queue_msg)
|
||||
" Received queue_msg = %s" % queue_msg)
|
||||
raise
|
||||
msg = result['msg']
|
||||
return msg
|
||||
|
||||
|
||||
def _plot_data(self, get):
|
||||
'''
|
||||
'''
|
||||
AJAX handler for the plot button
|
||||
Args:
|
||||
get (dict) - should have key 'submission' which contains the student submission
|
||||
Returns:
|
||||
dict - 'success' - whether or not we successfully queued this submission
|
||||
- 'message' - message to be rendered in case of error
|
||||
- 'message' - message to be rendered in case of error
|
||||
'''
|
||||
# only send data if xqueue exists
|
||||
if self.system.xqueue is None:
|
||||
@@ -748,26 +760,25 @@ class MatlabInput(CodeInput):
|
||||
anonymous_student_id = self.system.anonymous_student_id
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
self.id)
|
||||
self.input_id)
|
||||
xheader = xqueue_interface.make_xheader(
|
||||
lms_callback_url = callback_url,
|
||||
lms_key = queuekey,
|
||||
queue_name = self.queuename)
|
||||
lms_callback_url=callback_url,
|
||||
lms_key=queuekey,
|
||||
queue_name=self.queuename)
|
||||
|
||||
# save the input state
|
||||
self.input_state['queuekey'] = queuekey
|
||||
self.input_state['queuestate'] = 'queued'
|
||||
|
||||
|
||||
# construct xqueue body
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
'submission_time': qtime}
|
||||
contents = {'grader_payload': self.plot_payload,
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': response}
|
||||
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body = json.dumps(contents))
|
||||
body=json.dumps(contents))
|
||||
|
||||
return {'success': error == 0, 'message': msg}
|
||||
|
||||
@@ -1026,7 +1037,7 @@ class DragAndDropInput(InputTypeBase):
|
||||
|
||||
if tag_type == 'draggable':
|
||||
dic['target_fields'] = [parse(target, 'target') for target in
|
||||
tag.iterchildren('target')]
|
||||
tag.iterchildren('target')]
|
||||
|
||||
return dic
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
style="display:none;"
|
||||
% endif
|
||||
/>
|
||||
${trailing_text | h}
|
||||
|
||||
<p class="status">
|
||||
% if status == 'unsubmitted':
|
||||
|
||||
@@ -156,6 +156,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
|
||||
'hidden': False,
|
||||
'do_math': False,
|
||||
'id': '1_2_1',
|
||||
'trailing_text': '',
|
||||
'size': None}
|
||||
|
||||
expected_solution_context = {'id': '1_solution_1'}
|
||||
|
||||
@@ -60,6 +60,7 @@ class OptionInputTest(unittest.TestCase):
|
||||
|
||||
def test_option_parsing(self):
|
||||
f = inputtypes.OptionInput.parse_options
|
||||
|
||||
def check(input, options):
|
||||
"""Take list of options, confirm that output is in the silly doubled format"""
|
||||
expected = [(o, o) for o in options]
|
||||
@@ -120,7 +121,6 @@ class ChoiceGroupTest(unittest.TestCase):
|
||||
self.check_group('checkboxgroup', 'checkbox', '[]')
|
||||
|
||||
|
||||
|
||||
class JavascriptInputTest(unittest.TestCase):
|
||||
'''
|
||||
The javascript input is a pretty straightforward pass-thru, but test it anyway
|
||||
@@ -182,10 +182,10 @@ class TextLineTest(unittest.TestCase):
|
||||
'hidden': False,
|
||||
'inline': False,
|
||||
'do_math': False,
|
||||
'trailing_text': '',
|
||||
'preprocessor': None}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_math_rendering(self):
|
||||
size = "42"
|
||||
preprocessorClass = "preParty"
|
||||
@@ -209,11 +209,49 @@ class TextLineTest(unittest.TestCase):
|
||||
'msg': '',
|
||||
'hidden': False,
|
||||
'inline': False,
|
||||
'trailing_text': '',
|
||||
'do_math': True,
|
||||
'preprocessor': {'class_name': preprocessorClass,
|
||||
'script_src': script}}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_trailing_text_rendering(self):
|
||||
size = "42"
|
||||
# store (xml_text, expected)
|
||||
trailing_text = []
|
||||
# standard trailing text
|
||||
trailing_text.append(('m/s', 'm/s'))
|
||||
# unicode trailing text
|
||||
trailing_text.append((u'\xc3', u'\xc3'))
|
||||
# html escaped trailing text
|
||||
# this is the only one we expect to change
|
||||
trailing_text.append(('a < b', 'a < b'))
|
||||
|
||||
for xml_text, expected_text in trailing_text:
|
||||
xml_str = u"""<textline id="prob_1_2"
|
||||
size="{size}"
|
||||
trailing_text="{tt}"
|
||||
/>""".format(size=size, tt=xml_text)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee', }
|
||||
the_input = lookup_tag('textline')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'BumbleBee',
|
||||
'status': 'unanswered',
|
||||
'size': size,
|
||||
'msg': '',
|
||||
'hidden': False,
|
||||
'inline': False,
|
||||
'do_math': False,
|
||||
'trailing_text': expected_text,
|
||||
'preprocessor': None}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class FileSubmissionTest(unittest.TestCase):
|
||||
'''
|
||||
@@ -230,7 +268,6 @@ class FileSubmissionTest(unittest.TestCase):
|
||||
/>""".format(af=allowed_files,
|
||||
rf=required_files,)
|
||||
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee.py',
|
||||
@@ -242,12 +279,12 @@ class FileSubmissionTest(unittest.TestCase):
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'status': 'queued',
|
||||
'msg': input_class.submitted_msg,
|
||||
'value': 'BumbleBee.py',
|
||||
'queue_len': '3',
|
||||
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
|
||||
'required_files': '["cookies.py"]'}
|
||||
'status': 'queued',
|
||||
'msg': input_class.submitted_msg,
|
||||
'value': 'BumbleBee.py',
|
||||
'queue_len': '3',
|
||||
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
|
||||
'required_files': '["cookies.py"]'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -288,19 +325,19 @@ class CodeInputTest(unittest.TestCase):
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': input_class.submitted_msg,
|
||||
'mode': mode,
|
||||
'linenumbers': linenumbers,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
'hidden': '',
|
||||
'tabsize': int(tabsize),
|
||||
'queue_len': '3',
|
||||
}
|
||||
'status': 'queued',
|
||||
'msg': input_class.submitted_msg,
|
||||
'mode': mode,
|
||||
'linenumbers': linenumbers,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
'hidden': '',
|
||||
'tabsize': int(tabsize),
|
||||
'queue_len': '3'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class MatlabTest(unittest.TestCase):
|
||||
'''
|
||||
Test Matlab input types
|
||||
@@ -313,18 +350,18 @@ class MatlabTest(unittest.TestCase):
|
||||
self.payload = "payload"
|
||||
self.linenumbers = 'true'
|
||||
self.xml = """<matlabinput id="prob_1_2"
|
||||
rows="{r}" cols="{c}"
|
||||
rows="{r}" cols="{c}"
|
||||
tabsize="{tabsize}" mode="{m}"
|
||||
linenumbers="{ln}">
|
||||
<plot_payload>
|
||||
{payload}
|
||||
</plot_payload>
|
||||
</matlabinput>""".format(r = self.rows,
|
||||
c = self.cols,
|
||||
tabsize = self.tabsize,
|
||||
m = self.mode,
|
||||
payload = self.payload,
|
||||
ln = self.linenumbers)
|
||||
</matlabinput>""".format(r=self.rows,
|
||||
c=self.cols,
|
||||
tabsize=self.tabsize,
|
||||
m=self.mode,
|
||||
payload=self.payload,
|
||||
ln=self.linenumbers)
|
||||
elt = etree.fromstring(self.xml)
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
@@ -333,27 +370,24 @@ class MatlabTest(unittest.TestCase):
|
||||
self.input_class = lookup_tag('matlabinput')
|
||||
self.the_input = self.input_class(test_system, elt, state)
|
||||
|
||||
|
||||
def test_rendering(self):
|
||||
context = self.the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '3',
|
||||
}
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '3'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_rendering_with_state(self):
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
@@ -366,17 +400,16 @@ class MatlabTest(unittest.TestCase):
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': 'message',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '3',
|
||||
}
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': 'message',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '3'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -391,17 +424,16 @@ class MatlabTest(unittest.TestCase):
|
||||
context = the_input._get_render_context()
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.plot_submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '1',
|
||||
}
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.plot_submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '1'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
@@ -410,7 +442,7 @@ class MatlabTest(unittest.TestCase):
|
||||
response = self.the_input.handle_ajax("plot", get)
|
||||
|
||||
test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
|
||||
|
||||
|
||||
self.assertTrue(response['success'])
|
||||
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
|
||||
self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
|
||||
@@ -452,9 +484,6 @@ class MatlabTest(unittest.TestCase):
|
||||
self.assertFalse('queue_msg' in input_state)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SchematicTest(unittest.TestCase):
|
||||
'''
|
||||
Check that schematic inputs work
|
||||
@@ -468,7 +497,6 @@ class SchematicTest(unittest.TestCase):
|
||||
initial_value = 'two large batteries'
|
||||
submit_analyses = 'maybe'
|
||||
|
||||
|
||||
xml_str = """<schematic id="prob_1_2"
|
||||
height="{h}"
|
||||
width="{w}"
|
||||
@@ -498,8 +526,7 @@ class SchematicTest(unittest.TestCase):
|
||||
'height': height,
|
||||
'parts': parts,
|
||||
'analyses': analyses,
|
||||
'submit_analyses': submit_analyses,
|
||||
}
|
||||
'submit_analyses': submit_analyses}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
@@ -50,9 +50,7 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['p'],
|
||||
'targets': [
|
||||
'p_l', 'p_r'
|
||||
],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
@@ -91,18 +89,18 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
's_l[s][1]', 's_r[s][1]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'p_l[p][1]', 'p_l[p][3]', 'p_r[p][1]', 'p_r[p][3]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
@@ -133,18 +131,18 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
's_l[s][1]', 's_r[s][1]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'p_l[p][1]', 'p_l[p][3]', 'p_r[p][1]', 'p_r[p][3]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
@@ -188,22 +186,21 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][s_target][s][1]',
|
||||
'right_side_tagret[molecule][s_target][s][1]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][s_target][s][1]',
|
||||
'right_side_tagret[molecule][s_target][s][1]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][p_target][p][1]',
|
||||
'left_side_tagret[molecule][p_target][p][3]',
|
||||
'right_side_tagret[molecule][p_target][p][1]',
|
||||
'right_side_tagret[molecule][p_target][p][3]'
|
||||
],
|
||||
'rule': 'unordered_equal'
|
||||
'draggables': ['up'],
|
||||
'targets': [
|
||||
'left_side_tagret[molecule][p_target][p][1]',
|
||||
'left_side_tagret[molecule][p_target][p][3]',
|
||||
'right_side_tagret[molecule][p_target][p][1]',
|
||||
'right_side_tagret[molecule][p_target][p][3]'],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
@@ -242,32 +239,32 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
# up, up_and_down
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['triple_draggable'],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'unordered_equal'
|
||||
'draggables': ['triple_draggable'],
|
||||
'targets': ['p_l', 'p_r'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['double_draggable'],
|
||||
'targets': ['p_pi', 'p_pi*'],
|
||||
'rule': 'unordered_equal'
|
||||
'draggables': ['double_draggable'],
|
||||
'targets': ['p_pi', 'p_pi*'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['single_draggable'],
|
||||
'targets': ['s_l', 's_r', 's_sigma', 's_sigma*', 'p_sigma', 'p_sigma*'],
|
||||
'rule': 'unordered_equal'
|
||||
'draggables': ['single_draggable'],
|
||||
'targets': ['s_l', 's_r', 's_sigma', 's_sigma*', 'p_sigma', 'p_sigma*'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up'],
|
||||
'targets': ['p_l[triple_draggable][1]', 'p_l[triple_draggable][2]',
|
||||
'p_r[triple_draggable][2]', 'p_r[triple_draggable][3]'],
|
||||
'rule': 'unordered_equal'
|
||||
'draggables': ['up'],
|
||||
'targets': ['p_l[triple_draggable][1]', 'p_l[triple_draggable][2]',
|
||||
'p_r[triple_draggable][2]', 'p_r[triple_draggable][3]'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': ['s_l[single_draggable][1]', 's_r[single_draggable][1]',
|
||||
's_sigma[single_draggable][1]', 's_sigma*[single_draggable][1]',
|
||||
'p_pi[double_draggable][1]', 'p_pi[double_draggable][2]'],
|
||||
'rule': 'unordered_equal'
|
||||
'draggables': ['up_and_down'],
|
||||
'targets': ['s_l[single_draggable][1]', 's_r[single_draggable][1]',
|
||||
's_sigma[single_draggable][1]', 's_sigma*[single_draggable][1]',
|
||||
'p_pi[double_draggable][1]', 'p_pi[double_draggable][2]'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
|
||||
]
|
||||
@@ -290,7 +287,6 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
correct_answer = []
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
|
||||
def test_targets_false(self):
|
||||
user_input = '[{"1": "t1"}, \
|
||||
{"name_with_icon": "t2"}]'
|
||||
@@ -300,46 +296,44 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
def test_multiple_images_per_target_true(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \
|
||||
{"2": "t1"}]'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2',
|
||||
'2': 't1'}
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2', '2': 't1'}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_multiple_images_per_target_false(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}, \
|
||||
{"2": "t1"}]'
|
||||
correct_answer = {'1': 't2', 'name_with_icon': 't2',
|
||||
'2': 't1'}
|
||||
correct_answer = {'1': 't2', 'name_with_icon': 't2', '2': 't1'}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_targets_and_positions(self):
|
||||
user_input = '[{"1": [10,10]}, \
|
||||
{"name_with_icon": [[10,10],4]}]'
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [[10, 10], 4]}
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [[10, 10], 4]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_position_and_targets(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]'
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
|
||||
correct_answer = {'1': 't1', 'name_with_icon': 't2'}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_exact(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [20, 20]}
|
||||
correct_answer = {'1': [10, 10], 'name_with_icon': [20, 20]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_false(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [25, 25], 'name_with_icon': [20, 20]}
|
||||
correct_answer = {'1': [25, 25], 'name_with_icon': [20, 20]}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_true_in_radius(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [14, 14], 'name_with_icon': [20, 20]}
|
||||
correct_answer = {'1': [14, 14], 'name_with_icon': [20, 20]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_true_in_manual_radius(self):
|
||||
user_input = '[{"1": [10, 10]}, {"name_with_icon": [20, 20]}]'
|
||||
correct_answer = {'1': [[40, 10], 30], 'name_with_icon': [20, 20]}
|
||||
correct_answer = {'1': [[40, 10], 30], 'name_with_icon': [20, 20]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_positions_false_in_manual_radius(self):
|
||||
@@ -349,7 +343,7 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
|
||||
def test_correct_answer_not_has_key_from_user_answer(self):
|
||||
user_input = '[{"1": "t1"}, {"name_with_icon": "t2"}]'
|
||||
correct_answer = {'3': 't3', 'name_with_icon': 't2'}
|
||||
correct_answer = {'3': 't3', 'name_with_icon': 't2'}
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_anywhere(self):
|
||||
@@ -359,7 +353,7 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
user_input = '[{"ant":[610.5,57.449951171875]},\
|
||||
{"grass":[322.5,199.449951171875]}]'
|
||||
|
||||
correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
|
||||
correct_answer = {'grass': [[300, 200], 200], 'ant': [[500, 0], 200]}
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_lcao_correct(self):
|
||||
@@ -372,10 +366,10 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]'
|
||||
|
||||
correct_answer = [{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
'draggables': ['7', '8', '9', '10'],
|
||||
@@ -411,9 +405,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"12":"p_sigma_name"},{"14":"p_sigma_star_name"}]'
|
||||
|
||||
correct_answer = [{
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
'draggables': ['1', '2', '3', '4', '5', '6'],
|
||||
'targets': [
|
||||
's_left', 's_right', 's_sigma', 's_sigma_star', 'p_pi_1', 'p_pi_2'
|
||||
],
|
||||
'rule': 'anyof'
|
||||
}, {
|
||||
@@ -446,21 +440,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"2":"target2"},{"1":"target3"},{"2":"target4"},{"2":"target5"}, \
|
||||
{"3":"target6"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_reuse_draggable_with_mupliples(self):
|
||||
@@ -469,21 +464,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"2":"target2"},{"1":"target1"},{"2":"target4"},{"2":"target4"}, \
|
||||
{"3":"target6"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
{
|
||||
'draggables': ['1'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_reuse_many_draggable_with_mupliples(self):
|
||||
@@ -493,26 +489,27 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
|
||||
{"5": "target5"}, {"6": "target2"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_reuse_many_draggable_with_mupliples_wrong(self):
|
||||
@@ -524,26 +521,26 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"3":"target6"}, {"4": "target3"}, {"5": "target4"}, \
|
||||
{"5": "target5"}, {"6": "target2"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
{
|
||||
'draggables': ['1', '4'],
|
||||
'targets': ['target1', 'target3'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['2', '6'],
|
||||
'targets': ['target2', 'target4'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['5'],
|
||||
'targets': ['target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['3'],
|
||||
'targets': ['target6'],
|
||||
'rule': 'anyof'
|
||||
}]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_false(self):
|
||||
@@ -553,21 +550,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_(self):
|
||||
@@ -577,21 +575,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_multiple(self):
|
||||
@@ -601,21 +600,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_multiple_false(self):
|
||||
@@ -625,21 +625,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"c":"target6"}, {"a":"target7"},{"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target1"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a', 'a', 'a'],
|
||||
'targets': ['target1', 'target4', 'target7', 'target10'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'anyof+number'
|
||||
}
|
||||
]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_reused(self):
|
||||
@@ -649,21 +650,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_label_10_targets_with_a_b_c_reused_false(self):
|
||||
@@ -673,21 +675,22 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"c":"target6"}, {"b":"target8"},{"c":"target9"}, \
|
||||
{"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a', 'a'],
|
||||
'targets': ['target1', 'target10'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['b', 'b', 'b'],
|
||||
'targets': ['target2', 'target5', 'target8'],
|
||||
'rule': 'unordered_equal+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c', 'c', 'c'],
|
||||
'targets': ['target3', 'target6', 'target9'],
|
||||
'rule': 'unordered_equal+number'
|
||||
}
|
||||
]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_mixed_reuse_and_not_reuse(self):
|
||||
@@ -696,16 +699,17 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"},\
|
||||
{"a":"target5"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target5'],
|
||||
'rule': 'anyof'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_mixed_reuse_and_not_reuse_number(self):
|
||||
@@ -713,16 +717,17 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4'],
|
||||
'rule': 'anyof+number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_mixed_reuse_and_not_reuse_number_false(self):
|
||||
@@ -730,16 +735,17 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
user_input = '[{"a":"target1"}, \
|
||||
{"b":"target2"},{"c":"target3"}, {"a":"target4"}, {"a":"target10"}]'
|
||||
correct_answer = [
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target10'],
|
||||
'rule': 'anyof_number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}]
|
||||
{
|
||||
'draggables': ['a', 'a', 'b'],
|
||||
'targets': ['target1', 'target2', 'target4', 'target10'],
|
||||
'rule': 'anyof_number'
|
||||
},
|
||||
{
|
||||
'draggables': ['c'],
|
||||
'targets': ['target3'],
|
||||
'rule': 'exact'
|
||||
}
|
||||
]
|
||||
self.assertFalse(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
def test_alternative_correct_answer(self):
|
||||
@@ -747,9 +753,9 @@ class Test_DragAndDrop_Grade(unittest.TestCase):
|
||||
{"name_with_icon":"t1"},{"name_with_icon":"t1"},{"name4":"t1"}, \
|
||||
{"name4":"t1"}]'
|
||||
correct_answer = [
|
||||
{'draggables': ['name4'], 'targets': ['t1', 't1'], 'rule': 'exact'},
|
||||
{'draggables': ['name_with_icon'], 'targets': ['t1', 't1', 't1'],
|
||||
'rule': 'exact'}
|
||||
{'draggables': ['name4'], 'targets': ['t1', 't1'], 'rule': 'exact'},
|
||||
{'draggables': ['name_with_icon'], 'targets': ['t1', 't1', 't1'],
|
||||
'rule': 'exact'}
|
||||
]
|
||||
self.assertTrue(draganddrop.grade(user_input, correct_answer))
|
||||
|
||||
@@ -783,8 +789,8 @@ class Test_DraAndDrop_Compare_Positions(unittest.TestCase):
|
||||
def test_2a(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
self.assertTrue(dnd.compare_positions(correct=[[1, 1], [2, 3]],
|
||||
user=[[2, 3], [1, 1]],
|
||||
flag='exact'))
|
||||
user=[[2, 3], [1, 1]],
|
||||
flag='exact'))
|
||||
|
||||
def test_2b(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
@@ -813,8 +819,8 @@ class Test_DraAndDrop_Compare_Positions(unittest.TestCase):
|
||||
def test_6(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
self.assertTrue(dnd.compare_positions(correct=["a", "b", "c"],
|
||||
user=["a", "c", "b"],
|
||||
flag='anyof'))
|
||||
user=["a", "c", "b"],
|
||||
flag='anyof'))
|
||||
|
||||
def test_7(self):
|
||||
dnd = draganddrop.DragAndDrop({'1': 't1'}, '[{"1": "t1"}]')
|
||||
@@ -826,10 +832,9 @@ class Test_DraAndDrop_Compare_Positions(unittest.TestCase):
|
||||
def suite():
|
||||
|
||||
testcases = [Test_PositionsCompare,
|
||||
Test_DragAndDrop_Populate,
|
||||
Test_DragAndDrop_Grade,
|
||||
Test_DraAndDrop_Compare_Positions
|
||||
]
|
||||
Test_DragAndDrop_Populate,
|
||||
Test_DragAndDrop_Grade,
|
||||
Test_DraAndDrop_Compare_Positions]
|
||||
suites = []
|
||||
for testcase in testcases:
|
||||
suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase))
|
||||
|
||||
@@ -6,7 +6,7 @@ from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.exceptions import InvalidDefinitionError
|
||||
from xblock.core import String, Scope, Object, BlockScope
|
||||
from xblock.core import String, Scope, Object
|
||||
|
||||
DEFAULT = "_DEFAULT_GROUP"
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xblock.core import Scope, String
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -25,7 +24,6 @@ class AnnotatableModule(AnnotatableFields, XModule):
|
||||
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
|
||||
icon_class = 'annotatable'
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
XModule.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ class CapaFields(object):
|
||||
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
markdown = String(help="Markdown source of this module", scope=Scope.settings)
|
||||
source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings)
|
||||
|
||||
|
||||
class CapaModule(CapaFields, XModule):
|
||||
|
||||
@@ -106,10 +106,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
icon_class = 'problem'
|
||||
|
||||
js = {'coffee':
|
||||
[resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
]}
|
||||
[resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
]}
|
||||
js_module_name = "CombinedOpenEnded"
|
||||
|
||||
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
|
||||
@@ -219,5 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
|
||||
stores_state = True
|
||||
has_score = True
|
||||
always_recalculate_grades=True
|
||||
always_recalculate_grades = True
|
||||
template_dir_name = "combinedopenended"
|
||||
|
||||
@@ -10,7 +10,7 @@ from pkg_resources import resource_string
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xblock.core import String, Scope, List
|
||||
from xblock.core import Scope, List
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
@@ -60,8 +60,7 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
resource_string(__name__, 'js/src/conditional/display.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
|
||||
]}
|
||||
]}
|
||||
|
||||
js_module_name = "Conditional"
|
||||
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
||||
@@ -82,21 +81,24 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
xml_value = self.descriptor.xml_attributes.get(xml_attr)
|
||||
if xml_value:
|
||||
return xml_value, attr_name
|
||||
raise Exception('Error in conditional module: unknown condition "%s"'
|
||||
% xml_attr)
|
||||
raise Exception('Error in conditional module: unknown condition "%s"' % xml_attr)
|
||||
|
||||
def is_condition_satisfied(self):
|
||||
self.required_modules = [self.system.get_module(descriptor) for
|
||||
descriptor in self.descriptor.get_required_module_descriptors()]
|
||||
descriptor in self.descriptor.get_required_module_descriptors()]
|
||||
|
||||
xml_value, attr_name = self._get_condition()
|
||||
|
||||
if xml_value and self.required_modules:
|
||||
for module in self.required_modules:
|
||||
if not hasattr(module, attr_name):
|
||||
raise Exception('Error in conditional module: \
|
||||
required module {module} has no {module_attr}'.format(
|
||||
module=module, module_attr=attr_name))
|
||||
# We don't throw an exception here because it is possible for
|
||||
# the descriptor of a required module to have a property but
|
||||
# for the resulting module to be a (flavor of) ErrorModule.
|
||||
# So just log and return false.
|
||||
log.warn('Error in conditional module: \
|
||||
required module {module} has no {module_attr}'.format(module=module, module_attr=attr_name))
|
||||
return False
|
||||
|
||||
attr = getattr(module, attr_name)
|
||||
if callable(attr):
|
||||
@@ -111,7 +113,7 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
def get_html(self):
|
||||
# Calculate html ids of dependencies
|
||||
self.required_html_ids = [descriptor.location.html_id() for
|
||||
descriptor in self.descriptor.get_required_module_descriptors()]
|
||||
descriptor in self.descriptor.get_required_module_descriptors()]
|
||||
|
||||
return self.system.render_template('conditional_ajax.html', {
|
||||
'element_id': self.location.html_id(),
|
||||
@@ -130,7 +132,7 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
context = {'module': self,
|
||||
'message': message}
|
||||
html = self.system.render_template('conditional_module.html',
|
||||
context)
|
||||
context)
|
||||
return json.dumps({'html': [html], 'message': bool(message)})
|
||||
|
||||
html = [child.get_html() for child in self.get_display_items()]
|
||||
@@ -139,16 +141,15 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
|
||||
def get_icon_class(self):
|
||||
new_class = 'other'
|
||||
if self.is_condition_satisfied():
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
class_priority = ['video', 'problem']
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
class_priority = ['video', 'problem']
|
||||
|
||||
child_classes = [self.system.get_module(child_descriptor).get_icon_class()
|
||||
for child_descriptor in self.descriptor.get_children()]
|
||||
for c in class_priority:
|
||||
if c in child_classes:
|
||||
new_class = c
|
||||
child_classes = [self.system.get_module(child_descriptor).get_icon_class()
|
||||
for child_descriptor in self.descriptor.get_children()]
|
||||
for c in class_priority:
|
||||
if c in child_classes:
|
||||
new_class = c
|
||||
return new_class
|
||||
|
||||
|
||||
@@ -163,7 +164,6 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor):
|
||||
stores_state = True
|
||||
has_score = False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def parse_sources(xml_element, system, return_descriptor=False):
|
||||
"""Parse xml_element 'sources' attr and:
|
||||
|
||||
@@ -9,6 +9,7 @@ import StringIO
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from .django import contentstore
|
||||
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@@ -59,8 +60,9 @@ class StaticContent(object):
|
||||
@staticmethod
|
||||
def get_id_from_location(location):
|
||||
return {'tag': location.tag, 'org': location.org, 'course': location.course,
|
||||
'category': location.category, 'name': location.name,
|
||||
'revision': location.revision}
|
||||
'category': location.category, 'name': location.name,
|
||||
'revision': location.revision}
|
||||
|
||||
@staticmethod
|
||||
def get_location_from_path(path):
|
||||
# remove leading / character if it is there one
|
||||
@@ -79,8 +81,6 @@ class StaticContent(object):
|
||||
return StaticContent.get_url_path_from_location(loc)
|
||||
|
||||
|
||||
|
||||
|
||||
class ContentStore(object):
|
||||
'''
|
||||
Abstraction for all ContentStore providers (e.g. MongoDB)
|
||||
@@ -119,7 +119,7 @@ class ContentStore(object):
|
||||
thumbnail_name = StaticContent.generate_thumbnail_name(content.location.name)
|
||||
|
||||
thumbnail_file_location = StaticContent.compute_location(content.location.org, content.location.course,
|
||||
thumbnail_name, is_thumbnail=True)
|
||||
thumbnail_name, is_thumbnail=True)
|
||||
|
||||
# if we're uploading an image, then let's generate a thumbnail so that we can
|
||||
# serve it up when needed without having to rescale on the fly
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import absolute_import
|
||||
from importlib import import_module
|
||||
from os import environ
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from gridfs.errors import NoFile
|
||||
from xmodule.modulestore.mongo import location_to_query, Location
|
||||
from xmodule.contentstore.content import XASSET_LOCATION_TAG
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from .content import StaticContent, ContentStore
|
||||
@@ -26,7 +25,6 @@ class MongoContentStore(ContentStore):
|
||||
self.fs = gridfs.GridFS(_db)
|
||||
self.fs_files = _db["fs.files"] # the underlying collection GridFS uses
|
||||
|
||||
|
||||
def save(self, content):
|
||||
id = content.get_id()
|
||||
|
||||
@@ -34,7 +32,8 @@ class MongoContentStore(ContentStore):
|
||||
self.delete(id)
|
||||
|
||||
with self.fs.new_file(_id=id, filename=content.get_url_path(), content_type=content.content_type,
|
||||
displayname=content.name, thumbnail_location=content.thumbnail_location, import_path=content.import_path) as fp:
|
||||
displayname=content.name, thumbnail_location=content.thumbnail_location,
|
||||
import_path=content.import_path) as fp:
|
||||
|
||||
fp.write(content.data)
|
||||
|
||||
@@ -49,8 +48,9 @@ class MongoContentStore(ContentStore):
|
||||
try:
|
||||
with self.fs.get(id) as fp:
|
||||
return StaticContent(location, fp.displayname, fp.content_type, fp.read(),
|
||||
fp.uploadDate, thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
||||
import_path=fp.import_path if hasattr(fp, 'import_path') else None)
|
||||
fp.uploadDate,
|
||||
thumbnail_location=fp.thumbnail_location if hasattr(fp, 'thumbnail_location') else None,
|
||||
import_path=fp.import_path if hasattr(fp, 'import_path') else None)
|
||||
except NoFile:
|
||||
raise NotFoundError()
|
||||
|
||||
@@ -102,7 +102,7 @@ class MongoContentStore(ContentStore):
|
||||
]
|
||||
'''
|
||||
course_filter = Location(XASSET_LOCATION_TAG, category="asset" if not get_thumbnails else "thumbnail",
|
||||
course=location.course, org=location.org)
|
||||
course=location.course, org=location.org)
|
||||
# 'borrow' the function 'location_to_query' from the Mongo modulestore implementation
|
||||
items = self.fs_files.find(location_to_query(course_filter))
|
||||
return list(items)
|
||||
|
||||
@@ -211,7 +211,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
|
||||
template_dir_name = 'course'
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CourseDescriptor, self).__init__(*args, **kwargs)
|
||||
|
||||
@@ -421,7 +420,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
policy['GRADE_CUTOFFS'] = value
|
||||
self.grading_policy = policy
|
||||
|
||||
|
||||
@property
|
||||
def lowest_passing_grade(self):
|
||||
return min(self._grading_policy['GRADE_CUTOFFS'].values())
|
||||
@@ -460,7 +458,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
else:
|
||||
return self.cohort_config.get("auto_cohort_groups", [])
|
||||
|
||||
|
||||
@property
|
||||
def top_level_discussion_topic_ids(self):
|
||||
"""
|
||||
@@ -469,7 +466,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
topics = self.discussion_topics
|
||||
return [d["id"] for d in topics.values()]
|
||||
|
||||
|
||||
@property
|
||||
def cohorted_discussions(self):
|
||||
"""
|
||||
@@ -483,8 +479,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
|
||||
return set(config.get("cohorted_discussions", []))
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def is_newish(self):
|
||||
"""
|
||||
@@ -585,7 +579,6 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
yield module_descriptor
|
||||
|
||||
for c in self.get_children():
|
||||
sections = []
|
||||
for s in c.get_children():
|
||||
if s.lms.graded:
|
||||
xmoduledescriptors = list(yield_descriptor_descendents(s))
|
||||
@@ -601,8 +594,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
|
||||
all_descriptors.append(s)
|
||||
|
||||
return {'graded_sections': graded_sections,
|
||||
'all_descriptors': all_descriptors, }
|
||||
|
||||
'all_descriptors': all_descriptors, }
|
||||
|
||||
@staticmethod
|
||||
def make_id(org, course, url_name):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.editing_module import MetadataOnlyEditingDescriptor
|
||||
from xblock.core import String, Scope
|
||||
|
||||
|
||||
@@ -15,12 +15,11 @@ class DiscussionFields(object):
|
||||
|
||||
class DiscussionModule(DiscussionFields, XModule):
|
||||
js = {'coffee':
|
||||
[resource_string(__name__, 'js/src/time.coffee'),
|
||||
resource_string(__name__, 'js/src/discussion/display.coffee')]
|
||||
}
|
||||
[resource_string(__name__, 'js/src/time.coffee'),
|
||||
resource_string(__name__, 'js/src/discussion/display.coffee')]
|
||||
}
|
||||
js_module_name = "InlineDiscussion"
|
||||
|
||||
|
||||
def get_html(self):
|
||||
context = {
|
||||
'discussion_id': self.discussion_id,
|
||||
@@ -28,7 +27,7 @@ class DiscussionModule(DiscussionFields, XModule):
|
||||
return self.system.render_template('discussion/_discussion_module.html', context)
|
||||
|
||||
|
||||
class DiscussionDescriptor(DiscussionFields, RawDescriptor):
|
||||
class DiscussionDescriptor(DiscussionFields, MetadataOnlyEditingDescriptor, RawDescriptor):
|
||||
module_class = DiscussionModule
|
||||
template_dir_name = "discussion"
|
||||
|
||||
|
||||
@@ -41,6 +41,18 @@ class XMLEditingDescriptor(EditingDescriptor):
|
||||
js_module_name = "XMLEditingDescriptor"
|
||||
|
||||
|
||||
class MetadataOnlyEditingDescriptor(EditingDescriptor):
|
||||
"""
|
||||
Module which only provides an editing interface for the metadata, it does
|
||||
not expose a UI for editing the module data
|
||||
"""
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/raw/edit/metadata-only.coffee')]}
|
||||
js_module_name = "MetadataOnlyEditingDescriptor"
|
||||
|
||||
mako_template = "widgets/metadata-only-edit.html"
|
||||
|
||||
|
||||
class JSONEditingDescriptor(EditingDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of its data as XML. It does not perform
|
||||
|
||||
@@ -38,7 +38,7 @@ class ErrorModule(ErrorFields, XModule):
|
||||
'staff_access': True,
|
||||
'data': self.contents,
|
||||
'error': self.error_msg,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
class NonStaffErrorModule(ErrorFields, XModule):
|
||||
@@ -51,7 +51,7 @@ class NonStaffErrorModule(ErrorFields, XModule):
|
||||
'staff_access': False,
|
||||
'data': "",
|
||||
'error': "",
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
class ErrorDescriptor(ErrorFields, JSONEditingDescriptor):
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
class InvalidDefinitionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProcessingError(Exception):
|
||||
'''
|
||||
An error occurred while processing a request to the XModule.
|
||||
|
||||
@@ -51,6 +51,8 @@ class Date(ModelType):
|
||||
|
||||
|
||||
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
|
||||
|
||||
|
||||
class Timedelta(ModelType):
|
||||
def from_json(self, time_str):
|
||||
"""
|
||||
|
||||
@@ -107,7 +107,7 @@ class FolditModule(FolditFields, XModule):
|
||||
'show_leader': showleader,
|
||||
'folditbasic': self.get_basicpuzzles_html(),
|
||||
'folditchallenge': self.get_challenge_html()
|
||||
}
|
||||
}
|
||||
|
||||
return self.system.render_template('foldit.html', context)
|
||||
|
||||
@@ -124,7 +124,7 @@ class FolditModule(FolditFields, XModule):
|
||||
'success': self.is_complete(),
|
||||
'goal_level': goal_level,
|
||||
'completed': self.completed_puzzles(),
|
||||
}
|
||||
}
|
||||
return self.system.render_template('folditbasic.html', context)
|
||||
|
||||
def get_challenge_html(self):
|
||||
@@ -149,7 +149,6 @@ class FolditModule(FolditFields, XModule):
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for adding Foldit problems to courses
|
||||
|
||||
@@ -118,8 +118,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
html = file.read().decode('utf-8')
|
||||
# Log a warning if we can't parse the file, but don't error
|
||||
if not check_html(html):
|
||||
msg = "Couldn't parse html in {0}.".format(filepath)
|
||||
if not check_html(html) and len(html) > 0:
|
||||
msg = "Couldn't parse html in {0}, content = {1}".format(filepath, html)
|
||||
log.warning(msg)
|
||||
system.error_tracker("Warning: " + msg)
|
||||
|
||||
@@ -156,7 +156,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
|
||||
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(self.data.encode('utf-8'))
|
||||
html_data = self.data.encode('utf-8')
|
||||
file.write(html_data)
|
||||
|
||||
# write out the relative name
|
||||
relname = path(pathname).basename()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class @MetadataOnlyEditingDescriptor extends XModule.Descriptor
|
||||
constructor: (@element) ->
|
||||
|
||||
save: ->
|
||||
data: null
|
||||
@@ -252,7 +252,6 @@ class Location(_LocationBase):
|
||||
def __repr__(self):
|
||||
return "Location%s" % repr(tuple(self))
|
||||
|
||||
|
||||
@property
|
||||
def course_id(self):
|
||||
"""Return the ID of the Course that this item belongs to by looking
|
||||
@@ -414,7 +413,6 @@ class ModuleStore(object):
|
||||
return courses
|
||||
|
||||
|
||||
|
||||
class ModuleStoreBase(ModuleStore):
|
||||
'''
|
||||
Implement interface functionality that can be shared.
|
||||
@@ -425,6 +423,7 @@ class ModuleStoreBase(ModuleStore):
|
||||
'''
|
||||
self._location_errors = {} # location -> ErrorLog
|
||||
self.metadata_inheritance_cache = None
|
||||
self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes
|
||||
|
||||
def _get_errorlog(self, location):
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,6 @@ Passes settings.MODULESTORE as kwargs to MongoModuleStore
|
||||
|
||||
from __future__ import absolute_import
|
||||
from importlib import import_module
|
||||
from os import environ
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -38,7 +37,7 @@ def modulestore(name='default'):
|
||||
for key in FUNCTION_KEYS:
|
||||
if key in options:
|
||||
options[key] = load_function(options[key])
|
||||
|
||||
|
||||
_MODULESTORES[name] = class_(
|
||||
**options
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ from datetime import datetime
|
||||
from . import ModuleStoreBase, Location, namedtuple_to_son
|
||||
from .exceptions import ItemNotFoundError
|
||||
from .inheritance import own_metadata
|
||||
import logging
|
||||
|
||||
DRAFT = 'draft'
|
||||
|
||||
@@ -107,7 +106,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
|
||||
|
||||
def update_item(self, location, data):
|
||||
def update_item(self, location, data, allow_not_found=False):
|
||||
"""
|
||||
Set the data in the item specified by the location to
|
||||
data
|
||||
@@ -116,9 +115,13 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
data: A nested dictionary of problem data
|
||||
"""
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.clone_item(location, draft_loc)
|
||||
try:
|
||||
draft_item = self.get_item(location)
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.clone_item(location, draft_loc)
|
||||
except ItemNotFoundError, e:
|
||||
if not allow_not_found:
|
||||
raise e
|
||||
|
||||
return super(DraftModuleStore, self).update_item(draft_loc, data)
|
||||
|
||||
@@ -164,7 +167,6 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
return super(DraftModuleStore, self).delete_item(as_draft(location))
|
||||
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
@@ -178,6 +180,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
Save a current draft to the underlying modulestore
|
||||
"""
|
||||
draft = self.get_item(location)
|
||||
|
||||
draft.cms.published_date = datetime.utcnow()
|
||||
draft.cms.published_by = published_by_id
|
||||
super(DraftModuleStore, self).update_item(location, draft._model_data._kvs._data)
|
||||
@@ -221,6 +224,6 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
|
||||
# convert the dict - which is used for look ups - back into a list
|
||||
for key, value in to_process_dict.iteritems():
|
||||
queried_children.append(value)
|
||||
queried_children.append(value)
|
||||
|
||||
return queried_children
|
||||
|
||||
@@ -9,9 +9,10 @@ INHERITABLE_METADATA = (
|
||||
# intended to be set per-course, but can be overridden in for specific
|
||||
# elements. Can be a float.
|
||||
'days_early_for_beta',
|
||||
'giturl' # for git edit link
|
||||
'giturl' # for git edit link
|
||||
)
|
||||
|
||||
|
||||
def compute_inherited_metadata(descriptor):
|
||||
"""Given a descriptor, traverse all of its descendants and do metadata
|
||||
inheritance. Should be called on a CourseDescriptor after importing a
|
||||
|
||||
@@ -7,8 +7,8 @@ from collections import namedtuple
|
||||
from fs.osfs import OSFS
|
||||
from itertools import repeat
|
||||
from path import path
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
from uuid import uuid4
|
||||
|
||||
from importlib import import_module
|
||||
from xmodule.errortracker import null_error_tracker, exc_info_to_str
|
||||
@@ -31,6 +31,12 @@ log = logging.getLogger(__name__)
|
||||
# that assumption will have to change
|
||||
|
||||
|
||||
def get_course_id_no_run(location):
|
||||
'''
|
||||
'''
|
||||
return "/".join([location.org, location.course])
|
||||
|
||||
|
||||
class MongoKeyValueStore(KeyValueStore):
|
||||
"""
|
||||
A KeyValueStore that maps keyed data access to one of the 3 data areas
|
||||
@@ -125,8 +131,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
render_template: a function for rendering templates, as per
|
||||
MakoDescriptorSystem
|
||||
"""
|
||||
super(CachingDescriptorSystem, self).__init__(
|
||||
self.load_item, resources_fs, error_tracker, render_template)
|
||||
super(CachingDescriptorSystem, self).__init__(self.load_item, resources_fs,
|
||||
error_tracker, render_template)
|
||||
self.modulestore = modulestore
|
||||
self.module_data = module_data
|
||||
self.default_class = default_class
|
||||
@@ -135,7 +141,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
self.course_id = None
|
||||
self.cached_metadata = cached_metadata
|
||||
|
||||
|
||||
def load_item(self, location):
|
||||
"""
|
||||
Return an XModule instance for the specified location
|
||||
@@ -198,7 +203,9 @@ def location_to_query(location, wildcard=True):
|
||||
|
||||
if wildcard:
|
||||
for key, value in query.items():
|
||||
if value is None:
|
||||
# don't allow wildcards on revision, since public is set as None, so
|
||||
# its ambiguous between None as a real value versus None=wildcard
|
||||
if value is None and key != '_id.revision':
|
||||
del query[key]
|
||||
|
||||
return query
|
||||
@@ -216,7 +223,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
def __init__(self, host, db, collection, fs_root, render_template,
|
||||
port=27017, default_class=None,
|
||||
error_tracker=null_error_tracker,
|
||||
user=None, password=None, request_cache=None,
|
||||
user=None, password=None, request_cache=None,
|
||||
metadata_inheritance_cache_subsystem=None, **kwargs):
|
||||
|
||||
ModuleStoreBase.__init__(self)
|
||||
@@ -333,7 +340,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
'''
|
||||
key = metadata_cache_key(location)
|
||||
tree = {}
|
||||
|
||||
|
||||
if not force_refresh:
|
||||
# see if we are first in the request cache (if present)
|
||||
if self.request_cache is not None and key in self.request_cache.data.get('metadata_inheritance', {}):
|
||||
@@ -348,7 +355,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
if not tree:
|
||||
# if not in subsystem, or we are on force refresh, then we have to compute
|
||||
tree = self.compute_metadata_inheritance_tree(location)
|
||||
|
||||
|
||||
# now write out computed tree to caching subsystem (e.g. memcached), if available
|
||||
if self.metadata_inheritance_cache_subsystem is not None:
|
||||
self.metadata_inheritance_cache_subsystem.set(key, tree)
|
||||
@@ -461,7 +468,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't
|
||||
# bother with the metadata inheritance
|
||||
return [self._load_item(item, data_cache,
|
||||
apply_cached_metadata=(item['location']['category']!='course' or depth !=0)) for item in items]
|
||||
apply_cached_metadata=(item['location']['category'] != 'course' or depth != 0)) for item in items]
|
||||
|
||||
def get_courses(self):
|
||||
'''
|
||||
@@ -541,8 +548,15 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
Clone a new item that is a copy of the item at the location `source`
|
||||
and writes it to `location`
|
||||
"""
|
||||
item = None
|
||||
try:
|
||||
source_item = self.collection.find_one(location_to_query(source))
|
||||
|
||||
# allow for some programmatically generated substitutions in metadata, e.g. Discussion_id's should be auto-generated
|
||||
for key in source_item['metadata'].keys():
|
||||
if source_item['metadata'][key] == '$$GUID$$':
|
||||
source_item['metadata'][key] = uuid4().hex
|
||||
|
||||
source_item['_id'] = Location(location).dict()
|
||||
self.collection.insert(
|
||||
source_item,
|
||||
@@ -566,12 +580,19 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
course.tabs = existing_tabs
|
||||
self.update_metadata(course.location, course._model_data._kvs._metadata)
|
||||
|
||||
return item
|
||||
except pymongo.errors.DuplicateKeyError:
|
||||
raise DuplicateItemError(location)
|
||||
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
|
||||
|
||||
return item
|
||||
|
||||
def fire_updated_modulestore_signal(self, course_id, location):
|
||||
if self.modulestore_update_signal is not None:
|
||||
self.modulestore_update_signal.send(self, modulestore=self, course_id=course_id,
|
||||
location=location)
|
||||
|
||||
def get_course_for_item(self, location, depth=0):
|
||||
'''
|
||||
@@ -643,6 +664,8 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
self._update_single_item(location, {'definition.children': children})
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
# fire signal that we've written to DB
|
||||
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
|
||||
|
||||
def update_metadata(self, location, metadata):
|
||||
"""
|
||||
@@ -669,6 +692,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
self._update_single_item(location, {'metadata': metadata})
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.refresh_cached_metadata_inheritance_tree(loc)
|
||||
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
|
||||
|
||||
def delete_item(self, location):
|
||||
"""
|
||||
@@ -686,12 +710,12 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
course.tabs = [tab for tab in existing_tabs if tab.get('url_slug') != location.name]
|
||||
self.update_metadata(course.location, own_metadata(course))
|
||||
|
||||
self.collection.remove({'_id': Location(location).dict()},
|
||||
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
|
||||
# from overriding our default value set in the init method.
|
||||
safe=self.collection.safe)
|
||||
# Must include this to avoid the django debug toolbar (which defines the deprecated "safe=False")
|
||||
# from overriding our default value set in the init method.
|
||||
self.collection.remove({'_id': Location(location).dict()}, safe=self.collection.safe)
|
||||
# recompute (and update) the metadata inheritance tree which is cached
|
||||
self.refresh_cached_metadata_inheritance_tree(Location(location))
|
||||
self.fire_updated_modulestore_signal(get_course_id_no_run(Location(location)), Location(location))
|
||||
|
||||
def get_parent_locations(self, location, course_id):
|
||||
'''Find all locations that are the parents of this location in this
|
||||
@@ -699,7 +723,7 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
'''
|
||||
location = Location.ensure_fully_specified(location)
|
||||
items = self.collection.find({'definition.children': location.url()},
|
||||
{'_id': True})
|
||||
{'_id': True})
|
||||
return [i['_id'] for i in items]
|
||||
|
||||
def get_errored_courses(self):
|
||||
|
||||
@@ -3,7 +3,7 @@ from itertools import repeat
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from .exceptions import (ItemNotFoundError, NoPathToItem)
|
||||
from . import ModuleStore, Location
|
||||
from . import Location
|
||||
|
||||
|
||||
def path_to_location(modulestore, course_id, location):
|
||||
@@ -106,7 +106,7 @@ def path_to_location(modulestore, course_id, location):
|
||||
position_list = []
|
||||
for path_index in range(2, n - 1):
|
||||
category = path[path_index].category
|
||||
if category == 'sequential' or category == 'videosequence':
|
||||
if category == 'sequential' or category == 'videosequence':
|
||||
section_desc = modulestore.get_instance(course_id, path[path_index])
|
||||
child_locs = [c.location for c in section_desc.get_children()]
|
||||
# positions are 1-indexed, and should be strings to be consistent with
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
@@ -33,11 +32,11 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
|
||||
if original_loc.category != 'course':
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
course=dest_location.course)
|
||||
else:
|
||||
# on the course module we also have to update the module name
|
||||
module.location = module.location._replace(tag=dest_location.tag, org=dest_location.org,
|
||||
course=dest_location.course, name=dest_location.name)
|
||||
course=dest_location.course, name=dest_location.name)
|
||||
|
||||
print "Cloning module {0} to {1}....".format(original_loc, module.location)
|
||||
|
||||
@@ -49,9 +48,9 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
for child_loc_url in module.children:
|
||||
child_loc = Location(child_loc_url)
|
||||
child_loc = child_loc._replace(
|
||||
tag=dest_location.tag,
|
||||
org=dest_location.org,
|
||||
course=dest_location.course
|
||||
tag=dest_location.tag,
|
||||
org=dest_location.org,
|
||||
course=dest_location.course
|
||||
)
|
||||
new_children.append(child_loc.url())
|
||||
|
||||
@@ -67,7 +66,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
thumb_loc = Location(thumb["_id"])
|
||||
content = contentstore.find(thumb_loc)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning thumbnail {0} to {1}".format(thumb_loc, content.location)
|
||||
|
||||
@@ -80,12 +79,12 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
asset_loc = Location(asset["_id"])
|
||||
content = contentstore.find(asset_loc)
|
||||
content.location = content.location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
course=dest_location.course)
|
||||
|
||||
# be sure to update the pointer to the thumbnail
|
||||
if content.thumbnail_location is not None:
|
||||
content.thumbnail_location = content.thumbnail_location._replace(org=dest_location.org,
|
||||
course=dest_location.course)
|
||||
course=dest_location.course)
|
||||
|
||||
print "Cloning asset {0} to {1}".format(asset_loc, content.location)
|
||||
|
||||
@@ -94,7 +93,7 @@ def clone_course(modulestore, contentstore, source_location, dest_location, dele
|
||||
return True
|
||||
|
||||
|
||||
def delete_course(modulestore, contentstore, source_location, commit = False):
|
||||
def delete_course(modulestore, contentstore, source_location, commit=False):
|
||||
# first check to see if the modulestore is Mongo backed
|
||||
if not isinstance(modulestore, MongoModuleStore):
|
||||
raise Exception("Expected a MongoModuleStore in the runtime. Aborting....")
|
||||
|
||||
@@ -75,7 +75,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
|
||||
# tags that really need unique names--they store (or should store) state.
|
||||
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter',
|
||||
'videosequence', 'poll_question', 'timelimit')
|
||||
'videosequence', 'poll_question', 'timelimit')
|
||||
|
||||
attr = xml_data.attrib
|
||||
tag = xml_data.tag
|
||||
@@ -169,7 +169,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
# Didn't load properly. Fall back on loading as an error
|
||||
# descriptor. This should never error due to formatting.
|
||||
|
||||
|
||||
msg = "Error loading from xml. " + str(err)[:200]
|
||||
log.warning(msg)
|
||||
# Normally, we don't want lots of exception traces in our logs from common
|
||||
@@ -367,7 +366,7 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
|
||||
if org is None:
|
||||
msg = ("No 'org' attribute set for course in {dir}. "
|
||||
"Using default 'edx'".format(dir=course_dir))
|
||||
"Using default 'edx'".format(dir=course_dir))
|
||||
log.warning(msg)
|
||||
tracker(msg)
|
||||
org = 'edx'
|
||||
@@ -376,10 +375,10 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
|
||||
if course is None:
|
||||
msg = ("No 'course' attribute set for course in {dir}."
|
||||
" Using default '{default}'".format(
|
||||
dir=course_dir,
|
||||
default=course_dir
|
||||
))
|
||||
" Using default '{default}'".format(dir=course_dir,
|
||||
default=course_dir
|
||||
)
|
||||
)
|
||||
log.warning(msg)
|
||||
tracker(msg)
|
||||
course = course_dir
|
||||
@@ -445,7 +444,6 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
log.debug('========> Done with course import from {0}'.format(course_dir))
|
||||
return course_descriptor
|
||||
|
||||
|
||||
def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name):
|
||||
self._load_extra_content(system, course_descriptor, category, base_dir, course_dir)
|
||||
|
||||
@@ -453,7 +451,6 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
if os.path.isdir(base_dir / url_name):
|
||||
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
|
||||
|
||||
|
||||
def _load_extra_content(self, system, course_descriptor, category, path, course_dir):
|
||||
|
||||
for filepath in glob.glob(path / '*'):
|
||||
@@ -480,7 +477,6 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
|
||||
|
||||
def get_instance(self, course_id, location, depth=0):
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at
|
||||
@@ -542,7 +538,6 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_courses(self, depth=0):
|
||||
"""
|
||||
Returns a list of course descriptors. If there were errors on loading,
|
||||
@@ -567,7 +562,6 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
|
||||
def update_children(self, location, children):
|
||||
"""
|
||||
Set the children for the item specified by the location to
|
||||
@@ -578,7 +572,6 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
|
||||
def update_metadata(self, location, metadata):
|
||||
"""
|
||||
Set the metadata for the item specified by the location to
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import logging
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from fs.osfs import OSFS
|
||||
from json import dumps
|
||||
|
||||
|
||||
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
|
||||
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir, draft_modulestore=None):
|
||||
|
||||
course = modulestore.get_item(course_location)
|
||||
|
||||
@@ -40,6 +39,24 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
policy = {'course/' + course.location.name: own_metadata(course)}
|
||||
course_policy.write(dumps(policy))
|
||||
|
||||
# export draft content
|
||||
# NOTE: this code assumes that verticals are the top most draftable container
|
||||
# should we change the application, then this assumption will no longer
|
||||
# be valid
|
||||
if draft_modulestore is not None:
|
||||
draft_verticals = draft_modulestore.get_items([None, course_location.org, course_location.course,
|
||||
'vertical', None, 'draft'])
|
||||
if len(draft_verticals) > 0:
|
||||
draft_course_dir = export_fs.makeopendir('drafts')
|
||||
for draft_vertical in draft_verticals:
|
||||
parent_locs = draft_modulestore.get_parent_locations(draft_vertical.location, course.location.course_id)
|
||||
logging.debug('parent_locs = {0}'.format(parent_locs))
|
||||
draft_vertical.xml_attributes['parent_sequential_url'] = Location(parent_locs[0]).url()
|
||||
sequential = modulestore.get_item(Location(parent_locs[0]))
|
||||
index = sequential.children.index(draft_vertical.location.url())
|
||||
draft_vertical.xml_attributes['index_in_children_list'] = str(index)
|
||||
draft_vertical.export_to_xml(draft_course_dir)
|
||||
|
||||
|
||||
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
|
||||
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
|
||||
|
||||
@@ -6,17 +6,17 @@ from path import path
|
||||
|
||||
from xblock.core import Scope
|
||||
|
||||
from .xml import XMLModuleStore
|
||||
from .exceptions import DuplicateItemError
|
||||
from .xml import XMLModuleStore, ImportSystem, ParentTracker
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from .inheritance import own_metadata
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_static_content(modules, course_loc, course_data_path, static_content_store, target_location_namespace,
|
||||
subpath='static', verbose=False):
|
||||
subpath='static', verbose=False):
|
||||
|
||||
remap_dict = {}
|
||||
|
||||
@@ -107,10 +107,10 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
|
||||
# the caller passed in
|
||||
if module.location.category != 'course':
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course)
|
||||
course=target_location_namespace.course)
|
||||
else:
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course, name=target_location_namespace.name)
|
||||
course=target_location_namespace.course, name=target_location_namespace.name)
|
||||
|
||||
# then remap children pointers since they too will be re-namespaced
|
||||
if module.has_children:
|
||||
@@ -119,7 +119,7 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
|
||||
for child in children_locs:
|
||||
child_loc = Location(child)
|
||||
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course)
|
||||
course=target_location_namespace.course)
|
||||
|
||||
new_locs.append(new_child_loc.url())
|
||||
|
||||
@@ -139,8 +139,7 @@ def import_module_from_xml(modulestore, static_content_store, course_data_path,
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module.data, str) or isinstance(module.data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module.data, lambda link: verify_content_links(module, course_data_path,
|
||||
static_content_store, link, remap_dict))
|
||||
lxml_rewrite_links(module.data, lambda link: verify_content_links(module, course_data_path, static_content_store, link, remap_dict))
|
||||
|
||||
for key in remap_dict.keys():
|
||||
module.data = module.data.replace(key, remap_dict[key])
|
||||
@@ -163,9 +162,9 @@ def import_course_from_xml(modulestore, static_content_store, course_data_path,
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
@@ -175,7 +174,8 @@ def import_course_from_xml(modulestore, static_content_store, course_data_path,
|
||||
|
||||
def import_from_xml(store, data_dir, course_dirs=None,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True, static_content_store=None, target_location_namespace=None, verbose=False):
|
||||
load_error_modules=True, static_content_store=None, target_location_namespace=None,
|
||||
verbose=False, draft_store=None):
|
||||
"""
|
||||
Import the specified xml data_dir into the "store" modulestore,
|
||||
using org and course as the location org and course.
|
||||
@@ -190,7 +190,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
"""
|
||||
|
||||
module_store = XMLModuleStore(
|
||||
xml_module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class=default_class,
|
||||
course_dirs=course_dirs,
|
||||
@@ -201,7 +201,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
# to enumerate the entire collection of course modules. It will be left as a TBD to implement that
|
||||
# method on XmlModuleStore.
|
||||
course_items = []
|
||||
for course_id in module_store.modules.keys():
|
||||
for course_id in xml_module_store.modules.keys():
|
||||
|
||||
if target_location_namespace is not None:
|
||||
pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course])
|
||||
@@ -222,7 +222,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
# Quick scan to get course module as we need some info from there. Also we need to make sure that the
|
||||
# course module is committed first into the store
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
for module in xml_module_store.modules[course_id].itervalues():
|
||||
if module.category == 'course':
|
||||
course_data_path = path(data_dir) / module.data_dir
|
||||
course_location = module.location
|
||||
@@ -235,15 +235,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
# if there is *any* tabs - then there at least needs to be some predefined ones
|
||||
if module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
|
||||
|
||||
|
||||
if hasattr(module, 'data'):
|
||||
store.update_item(module.location, module.data)
|
||||
store.update_children(module.location, module.children)
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
import_module(module, store, course_data_path, static_content_store)
|
||||
|
||||
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
|
||||
# so let's make sure we import in case there are no other references to it in the modules
|
||||
@@ -251,17 +247,16 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
course_items.append(module)
|
||||
|
||||
|
||||
# then import all the static content
|
||||
if static_content_store is not None:
|
||||
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_namespace_rename, subpath='static', verbose=verbose)
|
||||
import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_namespace_rename, subpath='static', verbose=verbose)
|
||||
|
||||
# finally loop through all the modules
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
for module in xml_module_store.modules[course_id].itervalues():
|
||||
|
||||
if module.category == 'course':
|
||||
# we've already saved the course module up at the top of the loop
|
||||
@@ -275,59 +270,149 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
if verbose:
|
||||
log.debug('importing module location {0}'.format(module.location))
|
||||
|
||||
content = {}
|
||||
for field in module.fields:
|
||||
if field.scope != Scope.content:
|
||||
continue
|
||||
try:
|
||||
content[field.name] = module._model_data[field.name]
|
||||
except KeyError:
|
||||
# Ignore any missing keys in _model_data
|
||||
pass
|
||||
import_module(module, store, course_data_path, static_content_store)
|
||||
|
||||
if 'data' in content:
|
||||
module_data = content['data']
|
||||
# now import any 'draft' items
|
||||
if draft_store is not None:
|
||||
import_course_draft(xml_module_store, draft_store, course_data_path,
|
||||
static_content_store, target_location_namespace if target_location_namespace is not None
|
||||
else course_location)
|
||||
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
remap_dict = {}
|
||||
|
||||
# use the rewrite_links as a utility means to enumerate through all links
|
||||
# in the module data. We use that to load that reference into our asset store
|
||||
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
|
||||
# do the rewrites natively in that code.
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module_data,
|
||||
lambda link: verify_content_links(module, course_data_path, static_content_store, link, remap_dict))
|
||||
|
||||
for key in remap_dict.keys():
|
||||
module_data = module_data.replace(key, remap_dict[key])
|
||||
|
||||
except Exception:
|
||||
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
|
||||
else:
|
||||
module_data = content
|
||||
|
||||
store.update_item(module.location, module_data)
|
||||
|
||||
if hasattr(module, 'children') and module.children != []:
|
||||
store.update_children(module.location, module.children)
|
||||
|
||||
# NOTE: It's important to use own_metadata here to avoid writing
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
finally:
|
||||
# turn back on all write signalling
|
||||
if pseudo_course_id in store.ignore_write_events_on_courses:
|
||||
if pseudo_course_id in store.ignore_write_events_on_courses:
|
||||
store.ignore_write_events_on_courses.remove(pseudo_course_id)
|
||||
store.refresh_cached_metadata_inheritance_tree(target_location_namespace if
|
||||
target_location_namespace is not None else course_location)
|
||||
store.refresh_cached_metadata_inheritance_tree(target_location_namespace if
|
||||
target_location_namespace is not None else course_location)
|
||||
|
||||
return xml_module_store, course_items
|
||||
|
||||
|
||||
def import_module(module, store, course_data_path, static_content_store, allow_not_found=False):
|
||||
content = {}
|
||||
for field in module.fields:
|
||||
if field.scope != Scope.content:
|
||||
continue
|
||||
try:
|
||||
content[field.name] = module._model_data[field.name]
|
||||
except KeyError:
|
||||
# Ignore any missing keys in _model_data
|
||||
pass
|
||||
|
||||
module_data = {}
|
||||
if 'data' in content:
|
||||
module_data = content['data']
|
||||
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
remap_dict = {}
|
||||
|
||||
# use the rewrite_links as a utility means to enumerate through all links
|
||||
# in the module data. We use that to load that reference into our asset store
|
||||
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
|
||||
# do the rewrites natively in that code.
|
||||
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
|
||||
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
|
||||
# no good, so we have to do this kludge
|
||||
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path, static_content_store, link, remap_dict))
|
||||
|
||||
for key in remap_dict.keys():
|
||||
module_data = module_data.replace(key, remap_dict[key])
|
||||
|
||||
except Exception:
|
||||
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
|
||||
else:
|
||||
module_data = content
|
||||
|
||||
if allow_not_found:
|
||||
store.update_item(module.location, module_data, allow_not_found=allow_not_found)
|
||||
else:
|
||||
store.update_item(module.location, module_data)
|
||||
|
||||
if hasattr(module, 'children') and module.children != []:
|
||||
store.update_children(module.location, module.children)
|
||||
|
||||
# NOTE: It's important to use own_metadata here to avoid writing
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(own_metadata(module)))
|
||||
|
||||
|
||||
def import_course_draft(xml_module_store, store, course_data_path, static_content_store, target_location_namespace):
|
||||
'''
|
||||
This will import all the content inside of the 'drafts' folder, if it exists
|
||||
NOTE: This is not a full course import, basically in our current application only verticals (and downwards)
|
||||
can be in draft. Therefore, we need to use slightly different call points into the import process_xml
|
||||
as we can't simply call XMLModuleStore() constructor (like we do for importing public content)
|
||||
'''
|
||||
draft_dir = course_data_path + "/drafts"
|
||||
if not os.path.exists(draft_dir):
|
||||
return
|
||||
|
||||
# create a new 'System' object which will manage the importing
|
||||
errorlog = make_error_tracker()
|
||||
system = ImportSystem(
|
||||
xml_module_store,
|
||||
target_location_namespace.course_id,
|
||||
draft_dir,
|
||||
{},
|
||||
errorlog.tracker,
|
||||
ParentTracker(),
|
||||
None,
|
||||
)
|
||||
|
||||
# now walk the /vertical directory where each file in there will be a draft copy of the Vertical
|
||||
for dirname, dirnames, filenames in os.walk(draft_dir + "/vertical"):
|
||||
for filename in filenames:
|
||||
module_path = os.path.join(dirname, filename)
|
||||
with open(module_path) as f:
|
||||
try:
|
||||
xml = f.read().decode('utf-8')
|
||||
descriptor = system.process_xml(xml)
|
||||
|
||||
def _import_module(module):
|
||||
module.location = module.location._replace(revision='draft')
|
||||
# make sure our parent has us in its list of children
|
||||
# this is to make sure private only verticals show up in the list of children since
|
||||
# they would have been filtered out from the non-draft store export
|
||||
if module.location.category == 'vertical':
|
||||
module.location = module.location._replace(revision=None)
|
||||
sequential_url = module.xml_attributes['parent_sequential_url']
|
||||
index = int(module.xml_attributes['index_in_children_list'])
|
||||
|
||||
seq_location = Location(sequential_url)
|
||||
|
||||
# IMPORTANT: Be sure to update the sequential in the NEW namespace
|
||||
seq_location = seq_location._replace(org=target_location_namespace.org,
|
||||
course=target_location_namespace.course
|
||||
)
|
||||
sequential = store.get_item(seq_location)
|
||||
|
||||
if module.location.url() not in sequential.children:
|
||||
sequential.children.insert(index, module.location.url())
|
||||
store.update_children(sequential.location, sequential.children)
|
||||
|
||||
del module.xml_attributes['parent_sequential_url']
|
||||
del module.xml_attributes['index_in_children_list']
|
||||
|
||||
import_module(module, store, course_data_path, static_content_store, allow_not_found=True)
|
||||
for child in module.get_children():
|
||||
_import_module(child)
|
||||
|
||||
# HACK: since we are doing partial imports of drafts
|
||||
# the vertical doesn't have the 'url-name' set in the attributes (they are normally in the parent
|
||||
# object, aka sequential), so we have to replace the location.name with the XML filename
|
||||
# that is part of the pack
|
||||
fn, fileExtension = os.path.splitext(filename)
|
||||
descriptor.location = descriptor.location._replace(name=fn)
|
||||
|
||||
_import_module(descriptor)
|
||||
|
||||
except Exception, e:
|
||||
logging.exception('There was an error. {0}'.format(unicode(e)))
|
||||
pass
|
||||
|
||||
return module_store, course_items
|
||||
|
||||
def remap_namespace(module, target_location_namespace):
|
||||
if target_location_namespace is None:
|
||||
@@ -337,20 +422,20 @@ def remap_namespace(module, target_location_namespace):
|
||||
# the caller passed in
|
||||
if module.location.category != 'course':
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course)
|
||||
course=target_location_namespace.course)
|
||||
else:
|
||||
module.location = module.location._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course, name=target_location_namespace.name)
|
||||
course=target_location_namespace.course, name=target_location_namespace.name)
|
||||
|
||||
# then remap children pointers since they too will be re-namespaced
|
||||
if hasattr(module,'children'):
|
||||
if hasattr(module, 'children'):
|
||||
children_locs = module.children
|
||||
if children_locs is not None and children_locs != []:
|
||||
new_locs = []
|
||||
for child in children_locs:
|
||||
child_loc = Location(child)
|
||||
new_child_loc = child_loc._replace(tag=target_location_namespace.tag, org=target_location_namespace.org,
|
||||
course=target_location_namespace.course)
|
||||
course=target_location_namespace.course)
|
||||
|
||||
new_locs.append(new_child_loc.url())
|
||||
|
||||
@@ -365,7 +450,7 @@ def allowed_metadata_by_category(category):
|
||||
'vertical': [],
|
||||
'chapter': ['start'],
|
||||
'sequential': ['due', 'format', 'start', 'graded']
|
||||
}.get(category,['*'])
|
||||
}.get(category, ['*'])
|
||||
|
||||
|
||||
def check_module_metadata_editability(module):
|
||||
@@ -380,7 +465,6 @@ def check_module_metadata_editability(module):
|
||||
|
||||
allowed = allowed + ['xml_attributes', 'display_name']
|
||||
err_cnt = 0
|
||||
my_metadata = dict(own_metadata(module))
|
||||
illegal_keys = set(own_metadata(module).keys()) - set(allowed)
|
||||
|
||||
if len(illegal_keys) > 0:
|
||||
@@ -423,7 +507,7 @@ def validate_data_source_path_existence(path, is_err=True, extra_msg=None):
|
||||
_cnt = 0
|
||||
if not os.path.exists(path):
|
||||
print ("{0}: Expected folder at {1}. {2}".format('ERROR' if is_err == True else 'WARNING', path, extra_msg if
|
||||
extra_msg is not None else ''))
|
||||
extra_msg is not None else ''))
|
||||
_cnt = 1
|
||||
return _cnt
|
||||
|
||||
@@ -435,13 +519,13 @@ def validate_data_source_paths(data_dir, course_dir):
|
||||
warn_cnt = 0
|
||||
err_cnt += validate_data_source_path_existence(course_path / 'static')
|
||||
warn_cnt += validate_data_source_path_existence(course_path / 'static/subs', is_err=False,
|
||||
extra_msg='Video captions (if they are used) will not work unless they are static/subs.')
|
||||
extra_msg='Video captions (if they are used) will not work unless they are static/subs.')
|
||||
return err_cnt, warn_cnt
|
||||
|
||||
|
||||
def perform_xlint(data_dir, course_dirs,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True):
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
load_error_modules=True):
|
||||
err_cnt = 0
|
||||
warn_cnt = 0
|
||||
|
||||
@@ -497,7 +581,6 @@ def perform_xlint(data_dir, course_dirs,
|
||||
print "WARN: Missing course marketing video. It is recommended that every course have a marketing video."
|
||||
warn_cnt += 1
|
||||
|
||||
|
||||
print "\n\n------------------------------------------\nVALIDATION SUMMARY: {0} Errors {1} Warnings\n".format(err_cnt, warn_cnt)
|
||||
|
||||
if err_cnt > 0:
|
||||
|
||||
@@ -29,6 +29,6 @@ class RawDescriptor(XmlDescriptor, XMLEditingDescriptor):
|
||||
line, offset = err.position
|
||||
msg = ("Unable to create xml for problem {loc}. "
|
||||
"Context: '{context}'".format(
|
||||
context=lines[line - 1][offset - 40:offset + 40],
|
||||
loc=self.location))
|
||||
context=lines[line - 1][offset - 40:offset + 40],
|
||||
loc=self.location))
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
metadata:
|
||||
display_name: Discussion Tag
|
||||
for: Topic-Level Student-Visible Label
|
||||
id: 6002x_group_discussion_by_this
|
||||
id: $$GUID$$
|
||||
discussion_category: Week 1
|
||||
data: |
|
||||
<discussion for="Topic-Level Student-Visible Label" id="6002x_group_discussion_by_this" discussion_category="Week 1" />
|
||||
<discussion />
|
||||
children: []
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: E-text Written in LaTeX
|
||||
source_processor_url: https://qisx.mit.edu:5443/latex2edx
|
||||
display_name: E-text Written in LaTeX
|
||||
source_code: |
|
||||
\subsection{Example of E-text in LaTeX}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Problem Written in LaTeX
|
||||
source_processor_url: https://studio-input-filter.mitx.mit.edu/latex2edx
|
||||
display_name: Problem Written in LaTeX
|
||||
source_code: |
|
||||
% Nearly any kind of edX problem can be authored using Latex as
|
||||
% the source language. Write latex as usual, including equations. The
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Problem with Adaptive Hint
|
||||
source_processor_url: https://qisx.mit.edu:5443/latex2edx
|
||||
source_code: |
|
||||
\subsection{Problem With Adaptive Hint}
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
|
||||
import json
|
||||
from path import path
|
||||
import unittest
|
||||
from fs.memoryfs import MemoryFS
|
||||
|
||||
from lxml import etree
|
||||
from ast import literal_eval
|
||||
from mock import Mock, patch
|
||||
from collections import defaultdict
|
||||
|
||||
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
|
||||
from xmodule.xml_module import is_pointer_tag
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
from xmodule.error_module import NonStaffErrorDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.conditional_module import ConditionalModule
|
||||
|
||||
from .test_export import DATA_DIR
|
||||
from xmodule.tests.test_export import DATA_DIR
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'conditional' # name of directory with course data
|
||||
COURSE = 'conditional' # name of directory with course data
|
||||
|
||||
from . import test_system
|
||||
|
||||
@@ -47,10 +43,118 @@ class DummySystem(ImportSystem):
|
||||
def render_template(self, template, context):
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
class ConditionalFactory(object):
|
||||
"""
|
||||
A helper class to create a conditional module and associated source and child modules
|
||||
to allow for testing.
|
||||
"""
|
||||
@staticmethod
|
||||
def create(system, source_is_error_module=False):
|
||||
"""
|
||||
return a dict of modules: the conditional with a single source and a single child.
|
||||
Keys are 'cond_module', 'source_module', and 'child_module'.
|
||||
|
||||
if the source_is_error_module flag is set, create a real ErrorModule for the source.
|
||||
"""
|
||||
# construct source descriptor and module:
|
||||
source_location = Location(["i4x", "edX", "conditional_test", "problem", "SampleProblem"])
|
||||
if source_is_error_module:
|
||||
# Make an error descriptor and module
|
||||
source_descriptor = NonStaffErrorDescriptor.from_xml('some random xml data',
|
||||
system,
|
||||
org=source_location.org,
|
||||
course=source_location.course,
|
||||
error_msg='random error message')
|
||||
source_module = source_descriptor.xmodule(system)
|
||||
else:
|
||||
source_descriptor = Mock()
|
||||
source_descriptor.location = source_location
|
||||
source_module = Mock()
|
||||
|
||||
# construct other descriptors:
|
||||
child_descriptor = Mock()
|
||||
cond_descriptor = Mock()
|
||||
cond_descriptor.get_required_module_descriptors = lambda: [source_descriptor, ]
|
||||
cond_descriptor.get_children = lambda: [child_descriptor, ]
|
||||
cond_descriptor.xml_attributes = {"attempted": "true"}
|
||||
|
||||
# create child module:
|
||||
child_module = Mock()
|
||||
child_module.get_html = lambda: '<p>This is a secret</p>'
|
||||
child_module.displayable_items = lambda: [child_module]
|
||||
module_map = {source_descriptor: source_module, child_descriptor: child_module}
|
||||
system.get_module = lambda descriptor: module_map[descriptor]
|
||||
|
||||
# construct conditional module:
|
||||
cond_location = Location(["i4x", "edX", "conditional_test", "conditional", "SampleConditional"])
|
||||
model_data = {'data': '<conditional/>'}
|
||||
cond_module = ConditionalModule(system, cond_location, cond_descriptor, model_data)
|
||||
|
||||
# return dict:
|
||||
return {'cond_module': cond_module,
|
||||
'source_module': source_module,
|
||||
'child_module': child_module }
|
||||
|
||||
|
||||
class ConditionalModuleTest(unittest.TestCase):
|
||||
class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
"""
|
||||
Make sure that conditional module works, using mocks for
|
||||
other modules.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
|
||||
def test_icon_class(self):
|
||||
'''verify that get_icon_class works independent of condition satisfaction'''
|
||||
modules = ConditionalFactory.create(self.test_system)
|
||||
for attempted in ["false", "true"]:
|
||||
for icon_class in [ 'other', 'problem', 'video']:
|
||||
modules['source_module'].is_attempted = attempted
|
||||
modules['child_module'].get_icon_class = lambda: icon_class
|
||||
self.assertEqual(modules['cond_module'].get_icon_class(), icon_class)
|
||||
|
||||
|
||||
def test_get_html(self):
|
||||
modules = ConditionalFactory.create(self.test_system)
|
||||
# because test_system returns the repr of the context dict passed to render_template,
|
||||
# we reverse it here
|
||||
html = modules['cond_module'].get_html()
|
||||
html_dict = literal_eval(html)
|
||||
self.assertEqual(html_dict['element_id'], 'i4x-edX-conditional_test-conditional-SampleConditional')
|
||||
self.assertEqual(html_dict['id'], 'i4x://edX/conditional_test/conditional/SampleConditional')
|
||||
self.assertEqual(html_dict['depends'], 'i4x-edX-conditional_test-problem-SampleProblem')
|
||||
|
||||
def test_handle_ajax(self):
|
||||
modules = ConditionalFactory.create(self.test_system)
|
||||
modules['source_module'].is_attempted = "false"
|
||||
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
|
||||
print "ajax: ", ajax
|
||||
html = ajax['html']
|
||||
self.assertFalse(any(['This is a secret' in item for item in html]))
|
||||
|
||||
# now change state of the capa problem to make it completed
|
||||
modules['source_module'].is_attempted = "true"
|
||||
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
|
||||
print "post-attempt ajax: ", ajax
|
||||
html = ajax['html']
|
||||
self.assertTrue(any(['This is a secret' in item for item in html]))
|
||||
|
||||
def test_error_as_source(self):
|
||||
'''
|
||||
Check that handle_ajax works properly if the source is really an ErrorModule,
|
||||
and that the condition is not satisfied.
|
||||
'''
|
||||
modules = ConditionalFactory.create(self.test_system, source_is_error_module=True)
|
||||
ajax = json.loads(modules['cond_module'].handle_ajax('', ''))
|
||||
html = ajax['html']
|
||||
self.assertFalse(any(['This is a secret' in item for item in html]))
|
||||
|
||||
|
||||
class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
"""
|
||||
Make sure ConditionalModule works, by loading data in from an XML-defined course.
|
||||
"""
|
||||
@staticmethod
|
||||
def get_system(load_error_modules=True):
|
||||
'''Get a dummy system'''
|
||||
@@ -106,7 +210,7 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
html_expect = "{'ajax_url': 'courses/course_id/modx/a_location', 'element_id': 'i4x-HarvardX-ER22x-conditional-condone', 'id': 'i4x://HarvardX/ER22x/conditional/condone', 'depends': 'i4x-HarvardX-ER22x-problem-choiceprob'}"
|
||||
self.assertEqual(html, html_expect)
|
||||
|
||||
gdi = module.get_display_items()
|
||||
gdi = module.get_display_items()
|
||||
print "gdi=", gdi
|
||||
|
||||
ajax = json.loads(module.handle_ajax('', ''))
|
||||
@@ -121,3 +225,4 @@ class ConditionalModuleTest(unittest.TestCase):
|
||||
print "post-attempt ajax: ", ajax
|
||||
html = ajax['html']
|
||||
self.assertTrue(any(['This is a secret' in item for item in html]))
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ def lazyproperty(fn):
|
||||
"""
|
||||
|
||||
attr_name = '_lazy_' + fn.__name__
|
||||
|
||||
@property
|
||||
def _lazyprop(self):
|
||||
if not hasattr(self, attr_name):
|
||||
|
||||
@@ -340,7 +340,9 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
|
||||
# cdodge: this is a list of metadata names which are 'system' metadata
|
||||
# and should not be edited by an end-user
|
||||
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft', 'xml_attributes']
|
||||
|
||||
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft',
|
||||
'discussion_id', 'xml_attributes']
|
||||
|
||||
# A list of descriptor attributes that must be equal for the descriptors to
|
||||
# be equal
|
||||
|
||||
@@ -110,8 +110,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
'name', 'slug')
|
||||
|
||||
metadata_to_strip = ('data_dir',
|
||||
# cdodge: @TODO: We need to figure out a way to export out 'tabs' and 'grading_policy' which is on the course
|
||||
'tabs', 'grading_policy', 'is_draft', 'published_by', 'published_date',
|
||||
'tabs', 'grading_policy', 'published_by', 'published_date',
|
||||
'discussion_blackouts', 'testcenter_info',
|
||||
# VS[compat] -- remove the below attrs once everything is in the CMS
|
||||
'course', 'org', 'url_name', 'filename',
|
||||
@@ -135,7 +134,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
'graded': bool_map,
|
||||
'hide_progress_tab': bool_map,
|
||||
'allow_anonymous': bool_map,
|
||||
'allow_anonymous_to_peers': bool_map
|
||||
'allow_anonymous_to_peers': bool_map,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -359,6 +359,8 @@ Supported fields at the course level
|
||||
* `auto_cohort_groups`: `["group name 1", "group name 2", ...]` If `cohorted` and `auto_cohort` is true, automatically put each student into a random group from the `auto_cohort_groups` list, creating the group if needed.
|
||||
* - `pdf_textbooks`
|
||||
- have pdf-based textbooks on tabs in the courseware. See below for details on config.
|
||||
* - `html_textbooks`
|
||||
- have html-based textbooks on tabs in the courseware. See below for details on config.
|
||||
|
||||
|
||||
Available metadata
|
||||
@@ -511,6 +513,7 @@ If you want to customize the courseware tabs displayed for your course, specify
|
||||
"name": "Exciting news"
|
||||
},
|
||||
{"type": "textbooks"},
|
||||
{"type": "html_textbooks"},
|
||||
{"type": "pdf_textbooks"}
|
||||
]
|
||||
|
||||
@@ -518,6 +521,7 @@ If you want to customize the courseware tabs displayed for your course, specify
|
||||
* The first two tabs must have types `"courseware"` and `"course_info"`, in that order, or the course will not load.
|
||||
* The `courseware` tab never has a name attribute -- it's always rendered as "Courseware" for consistency between courses.
|
||||
* The `textbooks` tab will actually generate one tab per textbook, using the textbook titles as names.
|
||||
* The `html_textbooks` tab will actually generate one tab per html_textbook. The tab name is found in the html textbook definition.
|
||||
* The `pdf_textbooks` tab will actually generate one tab per pdf_textbook. The tab name is found in the pdf textbook definition.
|
||||
* For static tabs, the `url_slug` will be the url that points to the tab. It can not be one of the existing courseware url types (even if those aren't used in your course). The static content will come from `tabs/{course_url_name}/{url_slug}.html`, or `tabs/{url_slug}.html` if that doesn't exist.
|
||||
* An Instructor tab will be automatically added at the end for course staff users.
|
||||
@@ -538,6 +542,8 @@ If you want to customize the courseware tabs displayed for your course, specify
|
||||
- Parameters `name`, `link`.
|
||||
* - `textbooks`
|
||||
- No parameters--generates tab names from book titles.
|
||||
* - `html_textbooks`
|
||||
- No parameters--generates tab names from html book definition. (See discussion below for configuration.)
|
||||
* - `pdf_textbooks`
|
||||
- No parameters--generates tab names from pdf book definition. (See discussion below for configuration.)
|
||||
* - `progress`
|
||||
@@ -550,7 +556,7 @@ If you want to customize the courseware tabs displayed for your course, specify
|
||||
*********
|
||||
Textbooks
|
||||
*********
|
||||
Support is currently provided for image-based and PDF-based textbooks. In addition to enabling the display of textbooks in tabs (see above), specific information about the location of textbook content must be configured.
|
||||
Support is currently provided for image-based, HTML-based and PDF-based textbooks. In addition to enabling the display of textbooks in tabs (see above), specific information about the location of textbook content must be configured.
|
||||
|
||||
Image-based Textbooks
|
||||
=====================
|
||||
@@ -623,6 +629,62 @@ The course content can then link to page 25 using the `customtag` element:
|
||||
<customtag book="0" page="25" impl="book"/>
|
||||
|
||||
|
||||
HTML-based Textbooks
|
||||
====================
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
HTML-based textbooks are configured at the course level in the policy file. The JSON markup consists of an array of maps, with each map corresponding to a separate textbook. There are two styles to presenting HTML-based material. The first way is as a single HTML on a tab, which requires only a tab title and a URL for configuration. A second way permits the display of multiple HTML files that should be displayed together on a single view. For this view, a side panel of links is available on the left, allowing selection of a particular HTML to view.
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
"html_textbooks": [
|
||||
{"tab_title": "Textbook 1",
|
||||
"url": "https://www.example.com/thiscourse/book1/book1.html" },
|
||||
{"tab_title": "Textbook 2",
|
||||
"chapters": [
|
||||
{ "title": "Chapter 1", "url": "https://www.example.com/thiscourse/book2/Chapter1.html" },
|
||||
{ "title": "Chapter 2", "url": "https://www.example.com/thiscourse/book2/Chapter2.html" },
|
||||
{ "title": "Chapter 3", "url": "https://www.example.com/thiscourse/book2/Chapter3.html" },
|
||||
{ "title": "Chapter 4", "url": "https://www.example.com/thiscourse/book2/Chapter4.html" },
|
||||
{ "title": "Chapter 5", "url": "https://www.example.com/thiscourse/book2/Chapter5.html" },
|
||||
{ "title": "Chapter 6", "url": "https://www.example.com/thiscourse/book2/Chapter6.html" },
|
||||
{ "title": "Chapter 7", "url": "https://www.example.com/thiscourse/book2/Chapter7.html" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Some notes:
|
||||
|
||||
* It is not a good idea to include a top-level URL and chapter-level URLs in the same textbook configuration.
|
||||
|
||||
Linking from Content
|
||||
--------------------
|
||||
|
||||
It is possible to add links to specific pages in a textbook by using a URL that encodes the index of the textbook, the chapter (if chapters are used), and the page number. For a book with no chapters, the URL is of the form `/course/htmlbook/${bookindex}`. For a book with chapters, use `/course/htmlbook/${bookindex}/chapter/${chapter}` for a specific chapter, or `/course/htmlbook/${bookindex}` will default to the first chapter.
|
||||
|
||||
For example, for the book with no chapters configured above, the textbook can be reached using the URL `/course/htmlbook/0`. Reaching the third chapter of the second book is accomplished with `/course/htmlbook/1/chapter/3`.
|
||||
|
||||
You can use a `customtag` to create a template for such links. For example, you can create a `htmlbook` template in the `customtag` directory, containing:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<img src="/static/images/icons/textbook_icon.png"/> More information given in <a href="/course/htmlbook/${book}">the text</a>.
|
||||
|
||||
And a `htmlchapter` template containing:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<img src="/static/images/icons/textbook_icon.png"/> More information given in <a href="/course/htmlbook/${book}/chapter/${chapter}">the text</a>.
|
||||
|
||||
The example pages can then be linked using the `customtag` element:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<customtag book="0" impl="htmlbook"/>
|
||||
<customtag book="1" chapter="3" impl="htmlchapter"/>
|
||||
|
||||
PDF-based Textbooks
|
||||
===================
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
'''
|
||||
Tests for student activation and login
|
||||
'''
|
||||
from django.test import TestCase
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import Registration, UserProfile
|
||||
from courseware.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory
|
||||
import json
|
||||
|
||||
|
||||
class LoginTest(TestCase):
|
||||
'''
|
||||
Test student.views.login_user() view
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Create one user and save it to the database
|
||||
self.user = User.objects.create_user('test', 'test@edx.org', 'test_password')
|
||||
self.user.is_active = True
|
||||
self.user = UserFactory.build(username='test', email='test@edx.org')
|
||||
self.user.set_password('test_password')
|
||||
self.user.save()
|
||||
|
||||
# Create a registration for the user
|
||||
Registration().register(self.user)
|
||||
RegistrationFactory(user=self.user)
|
||||
|
||||
# Create a profile for the user
|
||||
UserProfile(user=self.user).save()
|
||||
UserProfileFactory(user=self.user)
|
||||
|
||||
# Create the test client
|
||||
self.client = Client()
|
||||
@@ -42,19 +44,17 @@ class LoginTest(TestCase):
|
||||
response = self._login_response(unicode_email, 'test_password')
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
|
||||
def test_login_fail_no_user_exists(self):
|
||||
response = self._login_response('not_a_user@edx.org', 'test_password')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
|
||||
def test_login_fail_wrong_password(self):
|
||||
response = self._login_response('test@edx.org', 'wrong_password')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
self._assert_response(response, success=False,
|
||||
value='Email or password is incorrect')
|
||||
|
||||
def test_login_not_activated(self):
|
||||
|
||||
# De-activate the user
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
@@ -62,8 +62,7 @@ class LoginTest(TestCase):
|
||||
# Should now be unable to login
|
||||
response = self._login_response('test@edx.org', 'test_password')
|
||||
self._assert_response(response, success=False,
|
||||
value="This account has not been activated")
|
||||
|
||||
value="This account has not been activated")
|
||||
|
||||
def test_login_unicode_email(self):
|
||||
unicode_email = u'test@edx.org' + unichr(40960)
|
||||
@@ -76,6 +75,7 @@ class LoginTest(TestCase):
|
||||
self._assert_response(response, success=False)
|
||||
|
||||
def _login_response(self, email, password):
|
||||
''' Post the login info '''
|
||||
post_params = {'email': email, 'password': password}
|
||||
return self.client.post(self.url, post_params)
|
||||
|
||||
@@ -95,13 +95,13 @@ class LoginTest(TestCase):
|
||||
try:
|
||||
response_dict = json.loads(response.content)
|
||||
except ValueError:
|
||||
self.fail("Could not parse response content as JSON: %s"
|
||||
% str(response.content))
|
||||
self.fail("Could not parse response content as JSON: %s"
|
||||
% str(response.content))
|
||||
|
||||
if success is not None:
|
||||
self.assertEqual(response_dict['success'], success)
|
||||
|
||||
if value is not None:
|
||||
msg = ("'%s' did not contain '%s'" %
|
||||
(str(response_dict['value']), str(value)))
|
||||
msg = ("'%s' did not contain '%s'" %
|
||||
(str(response_dict['value']), str(value)))
|
||||
self.assertTrue(value in response_dict['value'], msg)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django_comment_client.models import Permission, Role
|
||||
from django_comment_client.models import Role
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -12,18 +12,19 @@ class Command(BaseCommand):
|
||||
if len(args) > 1:
|
||||
raise CommandError("Too many arguments")
|
||||
course_id = args[0]
|
||||
|
||||
administrator_role = Role.objects.get_or_create(name="Administrator", course_id=course_id)[0]
|
||||
moderator_role = Role.objects.get_or_create(name="Moderator", course_id=course_id)[0]
|
||||
community_ta_role = Role.objects.get_or_create(name="Community TA", course_id=course_id)[0]
|
||||
student_role = Role.objects.get_or_create(name="Student", course_id=course_id)[0]
|
||||
|
||||
for per in ["vote", "update_thread", "follow_thread", "unfollow_thread",
|
||||
"update_comment", "create_sub_comment", "unvote", "create_thread",
|
||||
"follow_commentable", "unfollow_commentable", "create_comment", ]:
|
||||
"update_comment", "create_sub_comment", "unvote", "create_thread",
|
||||
"follow_commentable", "unfollow_commentable", "create_comment", ]:
|
||||
student_role.add_permission(per)
|
||||
|
||||
for per in ["edit_content", "delete_thread", "openclose_thread",
|
||||
"endorse_comment", "delete_comment", "see_all_cohorts"]:
|
||||
"endorse_comment", "delete_comment", "see_all_cohorts"]:
|
||||
moderator_role.add_permission(per)
|
||||
|
||||
for per in ["manage_moderator"]:
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
import json
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class MockCommentServiceRequestHandler(BaseHTTPRequestHandler):
|
||||
'''
|
||||
A handler for Comment Service POST requests.
|
||||
'''
|
||||
protocol = "HTTP/1.0"
|
||||
|
||||
def do_POST(self):
|
||||
'''
|
||||
Handle a POST request from the client
|
||||
Used by the APIs for comment threads, commentables, comments,
|
||||
subscriptions, commentables, users
|
||||
'''
|
||||
# Retrieve the POST data into a dict.
|
||||
# It should have been sent in json format
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
data_string = self.rfile.read(length)
|
||||
post_dict = json.loads(data_string)
|
||||
|
||||
# Log the request
|
||||
logger.debug("Comment Service received POST request %s to path %s" %
|
||||
(json.dumps(post_dict), self.path))
|
||||
|
||||
# Every good post has at least an API key
|
||||
if 'api_key' in post_dict:
|
||||
response = self.server._response_str
|
||||
# Log the response
|
||||
logger.debug("Comment Service: sending response %s" % json.dumps(response))
|
||||
|
||||
# Send a response back to the client
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(response)
|
||||
|
||||
else:
|
||||
# Respond with failure
|
||||
self.send_response(500, 'Bad Request: does not contain API key')
|
||||
self.send_header('Content-type', 'text/plain')
|
||||
self.end_headers()
|
||||
return False
|
||||
|
||||
|
||||
class MockCommentServiceServer(HTTPServer):
|
||||
'''
|
||||
A mock Comment Service server that responds
|
||||
to POST requests to localhost.
|
||||
'''
|
||||
def __init__(self, port_num,
|
||||
response={'username': 'new', 'external_id': 1}):
|
||||
'''
|
||||
Initialize the mock Comment Service server instance.
|
||||
*port_num* is the localhost port to listen to
|
||||
*response* is a dictionary that will be JSON-serialized
|
||||
and sent in response to comment service requests.
|
||||
'''
|
||||
self._response_str = json.dumps(response)
|
||||
|
||||
handler = MockCommentServiceRequestHandler
|
||||
address = ('', port_num)
|
||||
HTTPServer.__init__(self, address, handler)
|
||||
|
||||
def shutdown(self):
|
||||
'''
|
||||
Stop the server and free up the port
|
||||
'''
|
||||
# First call superclass shutdown()
|
||||
HTTPServer.shutdown(self)
|
||||
|
||||
# We also need to manually close the socket
|
||||
self.socket.close()
|
||||
@@ -0,0 +1,59 @@
|
||||
import unittest
|
||||
import threading
|
||||
import json
|
||||
import urllib2
|
||||
from mock_cs_server import MockCommentServiceServer
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class MockCommentServiceServerTest(unittest.TestCase):
|
||||
'''
|
||||
A mock version of the Comment Service server that listens on a local
|
||||
port and responds with pre-defined grade messages.
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
# This is a test of the test setup,
|
||||
# so it does not need to run as part of the unit test suite
|
||||
# You can re-enable it by commenting out the line below
|
||||
raise SkipTest
|
||||
|
||||
# Create the server
|
||||
server_port = 4567
|
||||
self.server_url = 'http://127.0.0.1:%d' % server_port
|
||||
|
||||
# Start up the server and tell it that by default it should
|
||||
# return this as its json response
|
||||
self.expected_response = {'username': 'user100', 'external_id': '4'}
|
||||
self.server = MockCommentServiceServer(port_num=server_port,
|
||||
response=self.expected_response)
|
||||
|
||||
# Start the server in a separate daemon thread
|
||||
server_thread = threading.Thread(target=self.server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
# Stop the server, freeing up the port
|
||||
self.server.shutdown()
|
||||
|
||||
def test_new_user_request(self):
|
||||
"""
|
||||
Test the mock comment service using an example
|
||||
of how you would create a new user
|
||||
"""
|
||||
# Send a request
|
||||
values = {'username': u'user100', 'api_key': 'TEST_API_KEY',
|
||||
'external_id': '4', 'email': u'user100@edx.org'}
|
||||
data = json.dumps(values)
|
||||
headers = {'Content-Type': 'application/json', 'Content-Length': len(data)}
|
||||
req = urllib2.Request(self.server_url + '/api/v1/users/4', data, headers)
|
||||
|
||||
# Send the request to the mock cs server
|
||||
response = urllib2.urlopen(req)
|
||||
|
||||
# Receive the reply from the mock cs server
|
||||
response_dict = json.loads(response.read())
|
||||
|
||||
# You should have received the response specified in the setup above
|
||||
self.assertEqual(response_dict, self.expected_response)
|
||||
@@ -146,28 +146,16 @@ def sort_map_entries(category_map):
|
||||
|
||||
|
||||
def initialize_discussion_info(course):
|
||||
|
||||
global _DISCUSSIONINFO
|
||||
|
||||
# only cache in-memory discussion information for 10 minutes
|
||||
# this is because we need a short-term hack fix for
|
||||
# mongo-backed courseware whereby new discussion modules can be added
|
||||
# without LMS service restart
|
||||
|
||||
if _DISCUSSIONINFO[course.id]:
|
||||
timestamp = _DISCUSSIONINFO[course.id].get('timestamp', datetime.now())
|
||||
age = datetime.now() - timestamp
|
||||
# expire every 5 minutes
|
||||
if age.seconds < 300:
|
||||
return
|
||||
|
||||
course_id = course.id
|
||||
|
||||
discussion_id_map = {}
|
||||
unexpanded_category_map = defaultdict(list)
|
||||
|
||||
# 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)
|
||||
|
||||
for module in all_modules:
|
||||
skip_module = False
|
||||
|
||||
@@ -3,17 +3,12 @@
|
||||
# django management command: dump grades to csv files
|
||||
# for use by batch processes
|
||||
|
||||
import os
|
||||
import sys
|
||||
import string
|
||||
import datetime
|
||||
import json
|
||||
import csv
|
||||
|
||||
from instructor.views import *
|
||||
from instructor.views import get_student_grade_summary_data
|
||||
from courseware.courses import get_course_by_id
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
@@ -45,7 +40,7 @@ class Command(BaseCommand):
|
||||
request = self.DummyRequest()
|
||||
try:
|
||||
course = get_course_by_id(course_id)
|
||||
except Exception as err:
|
||||
except Exception:
|
||||
if course_id in modulestore().courses:
|
||||
course = modulestore().courses[course_id]
|
||||
else:
|
||||
|
||||
@@ -11,7 +11,6 @@ import requests
|
||||
from requests.status_codes import codes
|
||||
import urllib
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
|
||||
from StringIO import StringIO
|
||||
|
||||
@@ -21,7 +20,6 @@ from django.http import HttpResponse
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
import requests
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from courseware import grades
|
||||
@@ -36,11 +34,7 @@ from django_comment_client.models import (Role,
|
||||
from django_comment_client.utils import has_forum_access
|
||||
from psychometrics import psychoanalyze
|
||||
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
import xmodule.graders as xmgraders
|
||||
import track.views
|
||||
|
||||
@@ -48,14 +42,15 @@ from .offline_gradecalc import student_grades, offline_grades_available
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
# internal commands for managing forum roles:
|
||||
FORUM_ROLE_ADD = 'add'
|
||||
FORUM_ROLE_REMOVE = 'remove'
|
||||
|
||||
|
||||
def split_by_comma_and_whitespace(s):
|
||||
"""
|
||||
Return string s, split by , or whitespace
|
||||
"""
|
||||
return re.split(r'[\s,]', s)
|
||||
|
||||
|
||||
@@ -141,7 +136,7 @@ def instructor_dashboard(request, course_id):
|
||||
# 'beta', so adding it to get_access_group_name doesn't really make
|
||||
# sense.
|
||||
name = course_beta_test_group_name(course.location)
|
||||
(group, created) = Group.objects.get_or_create(name=name)
|
||||
(group, _) = Group.objects.get_or_create(name=name)
|
||||
return group
|
||||
|
||||
# process actions from form POST
|
||||
@@ -237,13 +232,13 @@ def instructor_dashboard(request, course_id):
|
||||
if '/' not in problem_to_reset: # allow state of modules other than problem to be reset
|
||||
problem_to_reset = "problem/" + problem_to_reset # but problem is the default
|
||||
try:
|
||||
(org, course_name, run) = course_id.split("/")
|
||||
(org, course_name, _) = course_id.split("/")
|
||||
module_state_key = "i4x://" + org + "/" + course_name + "/" + problem_to_reset
|
||||
module_to_reset = StudentModule.objects.get(student_id=student_to_reset.id,
|
||||
course_id=course_id,
|
||||
module_state_key=module_state_key)
|
||||
msg += "Found module to reset. "
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
msg += "<font color='red'>Couldn't find module with that urlname. </font>"
|
||||
|
||||
if "Delete student state for problem" in action:
|
||||
@@ -352,7 +347,7 @@ def instructor_dashboard(request, course_id):
|
||||
return_csv('', datatable, fp=fp)
|
||||
fp.seek(0)
|
||||
files = {'datafile': fp}
|
||||
msg2, dataset = _do_remote_gradebook(request.user, course, 'post-grades', files=files)
|
||||
msg2, _ = _do_remote_gradebook(request.user, course, 'post-grades', files=files)
|
||||
msg += msg2
|
||||
|
||||
|
||||
@@ -423,7 +418,7 @@ def instructor_dashboard(request, course_id):
|
||||
datatable = {'header': ['username', 'email'] + profkeys}
|
||||
def getdat(u):
|
||||
p = u.profile
|
||||
return [u.username, u.email] + [getattr(p,x,'') for x in profkeys]
|
||||
return [u.username, u.email] + [getattr(p, x, '') for x in profkeys]
|
||||
|
||||
datatable['data'] = [getdat(u) for u in enrolled_students]
|
||||
datatable['title'] = 'Student profile data for course %s' % course_id
|
||||
@@ -433,17 +428,17 @@ def instructor_dashboard(request, course_id):
|
||||
elif 'Download CSV of all responses to problem' in action:
|
||||
problem_to_dump = request.POST.get('problem_to_dump','')
|
||||
|
||||
if problem_to_dump[-4:]==".xml":
|
||||
problem_to_dump=problem_to_dump[:-4]
|
||||
if problem_to_dump[-4:] == ".xml":
|
||||
problem_to_dump = problem_to_dump[:-4]
|
||||
try:
|
||||
(org, course_name, run)=course_id.split("/")
|
||||
module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_dump
|
||||
(org, course_name, run) = course_id.split("/")
|
||||
module_state_key = "i4x://" + org + "/" + course_name + "/problem/" + problem_to_dump
|
||||
smdat = StudentModule.objects.filter(course_id=course_id,
|
||||
module_state_key=module_state_key)
|
||||
smdat = smdat.order_by('student')
|
||||
msg += "Found %d records to dump " % len(smdat)
|
||||
except Exception as err:
|
||||
msg+="<font color='red'>Couldn't find module with that urlname. </font>"
|
||||
msg += "<font color='red'>Couldn't find module with that urlname. </font>"
|
||||
msg += "<pre>%s</pre>" % escape(err)
|
||||
smdat = []
|
||||
|
||||
@@ -741,7 +736,7 @@ def _list_course_forum_members(course_id, rolename, datatable):
|
||||
# make sure datatable is set up properly for display first, before checking for errors
|
||||
datatable['header'] = ['Username', 'Full name', 'Roles']
|
||||
datatable['title'] = 'List of Forum {0}s in course {1}'.format(rolename, course_id)
|
||||
datatable['data'] = [];
|
||||
datatable['data'] = []
|
||||
try:
|
||||
role = Role.objects.get(name=rolename, course_id=course_id)
|
||||
except Role.DoesNotExist:
|
||||
@@ -923,7 +918,7 @@ def get_student_grade_summary_data(request, course, course_id, get_grades=True,
|
||||
datarow = [student.id, student.username, student.profile.name, student.email]
|
||||
try:
|
||||
datarow.append(student.externalauthmap.external_email)
|
||||
except: # ExternalAuthMap.DoesNotExist
|
||||
except: # ExternalAuthMap.DoesNotExist
|
||||
datarow.append('')
|
||||
|
||||
if get_grades:
|
||||
@@ -1040,7 +1035,8 @@ def _do_enroll_students(course, course_id, students, overload=False):
|
||||
datatable['data'] = [[x, status[x]] for x in status]
|
||||
datatable['title'] = 'Enrollment of students'
|
||||
|
||||
def sf(stat): return [x for x in status if status[x] == stat]
|
||||
def sf(stat):
|
||||
return [x for x in status if status[x] == stat]
|
||||
|
||||
data = dict(added=sf('added'), rejected=sf('rejected') + sf('exists'),
|
||||
deleted=sf('deleted'), datatable=datatable)
|
||||
@@ -1136,7 +1132,7 @@ def dump_grading_context(course):
|
||||
'''
|
||||
msg = "-----------------------------------------------------------------------------\n"
|
||||
msg += "Course grader:\n"
|
||||
|
||||
|
||||
msg += '%s\n' % course.grader.__class__
|
||||
graders = {}
|
||||
if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader):
|
||||
@@ -1151,7 +1147,7 @@ def dump_grading_context(course):
|
||||
|
||||
gc = course.grading_context
|
||||
msg += "graded sections:\n"
|
||||
|
||||
|
||||
msg += '%s\n' % gc['graded_sections'].keys()
|
||||
for (gs, gsvals) in gc['graded_sections'].items():
|
||||
msg += "--> Section %s:\n" % (gs)
|
||||
|
||||
@@ -27,8 +27,6 @@ from mitxmako.shortcuts import render_to_string
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
system = ModuleSystem(
|
||||
ajax_url=None,
|
||||
track_function=None,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from lxml import etree
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
@@ -38,6 +36,20 @@ def index_shifted(request, course_id, page):
|
||||
|
||||
@login_required
|
||||
def pdf_index(request, course_id, book_index, chapter=None, page=None):
|
||||
"""
|
||||
Display a PDF textbook.
|
||||
|
||||
course_id: course for which to display text. The course should have
|
||||
"pdf_textbooks" property defined.
|
||||
|
||||
book index: zero-based index of which PDF textbook to display.
|
||||
|
||||
chapter: (optional) one-based index into the chapter array of textbook PDFs to display.
|
||||
Defaults to first chapter. Specifying this assumes that there are separate PDFs for
|
||||
each chapter in a textbook.
|
||||
|
||||
page: (optional) one-based page number to display within the PDF. Defaults to first page.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
|
||||
@@ -63,7 +75,6 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
|
||||
for entry in textbook['chapters']:
|
||||
entry['url'] = remap_static_url(entry['url'], course)
|
||||
|
||||
|
||||
return render_to_response('static_pdfbook.html',
|
||||
{'book_index': book_index,
|
||||
'course': course,
|
||||
@@ -72,8 +83,21 @@ def pdf_index(request, course_id, book_index, chapter=None, page=None):
|
||||
'page': page,
|
||||
'staff_access': staff_access})
|
||||
|
||||
|
||||
@login_required
|
||||
def html_index(request, course_id, book_index, chapter=None, anchor_id=None):
|
||||
def html_index(request, course_id, book_index, chapter=None):
|
||||
"""
|
||||
Display an HTML textbook.
|
||||
|
||||
course_id: course for which to display text. The course should have
|
||||
"html_textbooks" property defined.
|
||||
|
||||
book index: zero-based index of which HTML textbook to display.
|
||||
|
||||
chapter: (optional) one-based index into the chapter array of textbook HTML files to display.
|
||||
Defaults to first chapter. Specifying this assumes that there are separate HTML files for
|
||||
each chapter in a textbook.
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'load')
|
||||
staff_access = has_access(request.user, course, 'staff')
|
||||
|
||||
@@ -99,11 +123,9 @@ def html_index(request, course_id, book_index, chapter=None, anchor_id=None):
|
||||
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})
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 226 KiB |
@@ -26,32 +26,22 @@
|
||||
// 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) {
|
||||
loadUrl = function htmlViewLoadUrl(url) {
|
||||
// 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) {
|
||||
loadChapterUrl = function htmlViewLoadChapterUrl(chapterNum) {
|
||||
if (chapterNum < 1 || chapterNum > chapterUrls.length) {
|
||||
return;
|
||||
}
|
||||
var chapterUrl = chapterUrls[chapterNum-1];
|
||||
loadUrl(chapterUrl, anchorId);
|
||||
loadUrl(chapterUrl);
|
||||
};
|
||||
|
||||
// define navigation links for chapters:
|
||||
@@ -64,15 +54,15 @@
|
||||
};
|
||||
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);
|
||||
loadUrl(urlToLoad);
|
||||
} else {
|
||||
loadChapterUrl(chapterToLoad, anchorToLoad);
|
||||
}
|
||||
loadChapterUrl(chapterToLoad);
|
||||
}
|
||||
|
||||
}
|
||||
})(jQuery);
|
||||
@@ -92,9 +82,6 @@
|
||||
%if chapter is not None:
|
||||
options.chapterNum = ${chapter};
|
||||
%endif
|
||||
%if anchor_id is not None:
|
||||
options.anchor_id = ${anchor_id};
|
||||
%endif
|
||||
|
||||
$('#outerContainer').myHTMLViewer(options);
|
||||
});
|
||||
|
||||
@@ -267,10 +267,6 @@ if settings.COURSEWARE_ENABLED:
|
||||
'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/?$',
|
||||
'courseware.views.index', name="courseware"),
|
||||
|
||||
Reference in New Issue
Block a user