Merge branch 'master' into bug/christina/studio
Conflicts: cms/djangoapps/contentstore/views.py common/lib/xmodule/xmodule/capa_module.py common/lib/xmodule/xmodule/combined_open_ended_module.py common/lib/xmodule/xmodule/peer_grading_module.py
This commit is contained in:
@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore
|
||||
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.modulestore.xml_importer import import_from_xml, perform_xlint
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
def test_edit_unit_full(self):
|
||||
self.check_edit_unit('full')
|
||||
|
||||
def _get_draft_counts(self, item):
|
||||
cnt = 1 if getattr(item, 'is_draft', False) else 0
|
||||
for child in item.get_children():
|
||||
cnt = cnt + self._get_draft_counts(child)
|
||||
|
||||
return cnt
|
||||
|
||||
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)
|
||||
|
||||
# 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]))
|
||||
|
||||
# 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))
|
||||
|
||||
#now requery with depth
|
||||
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)
|
||||
|
||||
|
||||
def test_static_tab_reordering(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# check that there's actually content in the 'question' field
|
||||
self.assertGreater(len(items[0].question),0)
|
||||
|
||||
def test_xlint_fails(self):
|
||||
err_cnt = perform_xlint('common/test/data', ['full'])
|
||||
self.assertGreater(err_cnt, 0)
|
||||
|
||||
def test_delete(self):
|
||||
import_from_xml(modulestore(), 'common/test/data/', ['full'])
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
'''
|
||||
Utilities for contentstore tests
|
||||
'''
|
||||
|
||||
#pylint: disable=W0603
|
||||
|
||||
import json
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase):
|
||||
collection with templates before running the TestCase
|
||||
and drops it they are finished. """
|
||||
|
||||
def _pre_setup(self):
|
||||
super(ModuleStoreTestCase, self)._pre_setup()
|
||||
@staticmethod
|
||||
def flush_mongo_except_templates():
|
||||
'''
|
||||
Delete everything in the module store except templates
|
||||
'''
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
|
||||
# This query means: every item in the collection
|
||||
# that is not a template
|
||||
query = {"_id.course": {"$ne": "templates"}}
|
||||
|
||||
# Remove everything except templates
|
||||
modulestore.collection.remove(query)
|
||||
|
||||
@staticmethod
|
||||
def load_templates_if_necessary():
|
||||
'''
|
||||
Load templates into the modulestore only if they do not already exist.
|
||||
We need the templates, because they are copied to create
|
||||
XModules such as sections and problems
|
||||
'''
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
|
||||
# Count the number of templates
|
||||
query = {"_id.course": "templates"}
|
||||
num_templates = modulestore.collection.find(query).count()
|
||||
|
||||
if num_templates < 1:
|
||||
update_templates()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
'''
|
||||
Flush the mongo store and set up templates
|
||||
'''
|
||||
|
||||
# Use a uuid to differentiate
|
||||
# the mongo collections on jenkins.
|
||||
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
self.test_MODULESTORE = self.orig_MODULESTORE
|
||||
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
settings.MODULESTORE = self.test_MODULESTORE
|
||||
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
|
||||
test_modulestore = cls.orig_modulestore
|
||||
test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
test_modulestore['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
update_templates()
|
||||
|
||||
settings.MODULESTORE = test_modulestore
|
||||
|
||||
TestCase.setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
'''
|
||||
Revert to the old modulestore settings
|
||||
'''
|
||||
|
||||
# Clean up by dropping the collection
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
modulestore.collection.drop()
|
||||
|
||||
# Restore the original modulestore settings
|
||||
settings.MODULESTORE = cls.orig_modulestore
|
||||
|
||||
def _pre_setup(self):
|
||||
'''
|
||||
Remove everything but the templates before each test
|
||||
'''
|
||||
|
||||
# Flush anything that is not a template
|
||||
ModuleStoreTestCase.flush_mongo_except_templates()
|
||||
|
||||
# Check that we have templates loaded; if not, load them
|
||||
ModuleStoreTestCase.load_templates_if_necessary()
|
||||
|
||||
# Call superclass implementation
|
||||
super(ModuleStoreTestCase, self)._pre_setup()
|
||||
|
||||
def _post_teardown(self):
|
||||
# Make sure you flush out the modulestore.
|
||||
# Drop the collection at the end of the test,
|
||||
# otherwise there will be lingering collections leftover
|
||||
# from executing the tests.
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
settings.MODULESTORE = self.orig_MODULESTORE
|
||||
'''
|
||||
Flush everything we created except the templates
|
||||
'''
|
||||
# Flush anything that is not a template
|
||||
ModuleStoreTestCase.flush_mongo_except_templates()
|
||||
|
||||
# Call superclass implementation
|
||||
super(ModuleStoreTestCase, self)._post_teardown()
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from django.core.urlresolvers import reverse
|
||||
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"}
|
||||
|
||||
def get_modulestore(location):
|
||||
"""
|
||||
@@ -137,7 +141,7 @@ def compute_unit_state(unit):
|
||||
'private' content is editabled and not visible in the LMS
|
||||
"""
|
||||
|
||||
if unit.cms.is_draft:
|
||||
if getattr(unit, 'is_draft', False):
|
||||
try:
|
||||
modulestore('direct').get_item(unit.location)
|
||||
return UnitState.draft
|
||||
@@ -187,3 +191,35 @@ class CoursePageNames:
|
||||
SettingsGrading = "settings_grading"
|
||||
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.
|
||||
@param course: A course object from the modulestore.
|
||||
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
|
||||
"""
|
||||
#Copy course tabs
|
||||
course_tabs = copy.copy(course.tabs)
|
||||
changed = False
|
||||
#Check to see if open ended panel is defined in the course
|
||||
if OPEN_ENDED_PANEL not in course_tabs:
|
||||
#Add panel to the tabs if it is not defined
|
||||
course_tabs.append(OPEN_ENDED_PANEL)
|
||||
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.
|
||||
@param course: A course object from the modulestore.
|
||||
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
|
||||
"""
|
||||
#Copy course tabs
|
||||
course_tabs = copy.copy(course.tabs)
|
||||
changed = False
|
||||
#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]
|
||||
changed = True
|
||||
return changed, course_tabs
|
||||
|
||||
@@ -41,7 +41,7 @@ from xmodule.modulestore.mongo import MongoUsage
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule_modifiers import replace_static_urls, wrap_xmodule
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from functools import partial
|
||||
|
||||
from xmodule.contentstore.django import contentstore
|
||||
@@ -52,7 +52,8 @@ from auth.authz import is_user_in_course_group_role, get_users_in_course_group_b
|
||||
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
|
||||
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
|
||||
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
|
||||
UnitState, get_course_for_item, get_url_reverse
|
||||
UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
|
||||
remove_open_ended_panel_tab
|
||||
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from contentstore.course_info_model import get_course_updates, \
|
||||
@@ -73,7 +74,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading']
|
||||
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@@ -188,7 +190,7 @@ def course_index(request, org, course, name):
|
||||
'coursename': name
|
||||
})
|
||||
|
||||
course = modulestore().get_item(location)
|
||||
course = modulestore().get_item(location, depth=3)
|
||||
sections = course.get_children()
|
||||
|
||||
return render_to_response('overview.html', {
|
||||
@@ -208,19 +210,14 @@ def course_index(request, org, course, name):
|
||||
@login_required
|
||||
def edit_subsection(request, location):
|
||||
# check that we have permissions to edit this item
|
||||
if not has_access(request.user, location):
|
||||
course = get_course_for_item(location)
|
||||
if not has_access(request.user, course.location):
|
||||
raise PermissionDenied()
|
||||
|
||||
item = modulestore().get_item(location)
|
||||
item = modulestore().get_item(location, depth=1)
|
||||
|
||||
# TODO: we need a smarter way to figure out what course an item is in
|
||||
for course in modulestore().get_courses():
|
||||
if (course.location.org == item.location.org and
|
||||
course.location.course == item.location.course):
|
||||
break
|
||||
|
||||
lms_link = get_lms_link_for_item(location)
|
||||
preview_link = get_lms_link_for_item(location, preview=True)
|
||||
lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
|
||||
preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
|
||||
|
||||
# make sure that location references a 'sequential', otherwise return BadRequest
|
||||
if item.location.category != 'sequential':
|
||||
@@ -253,12 +250,6 @@ def edit_subsection(request, location):
|
||||
can_view_live = True
|
||||
break
|
||||
|
||||
# item.lms.start is a struct_time using GMT
|
||||
# item.lms.due is a String, 'March 20 17:00'
|
||||
|
||||
# edit_subsection.html, due is converted to dateutil.parser.parse(item.lms.due) = {datetime} 2013-03-20 17:00:00
|
||||
#parsed_due_date = dateutil.parser.parse(item.lms.due)
|
||||
|
||||
return render_to_response('edit_subsection.html',
|
||||
{'subsection': item,
|
||||
'context_course': course,
|
||||
@@ -283,19 +274,13 @@ def edit_unit(request, location):
|
||||
|
||||
id: A Location URL
|
||||
"""
|
||||
# check that we have permissions to edit this item
|
||||
if not has_access(request.user, location):
|
||||
course = get_course_for_item(location)
|
||||
if not has_access(request.user, course.location):
|
||||
raise PermissionDenied()
|
||||
|
||||
item = modulestore().get_item(location)
|
||||
item = modulestore().get_item(location, depth=1)
|
||||
|
||||
# TODO: we need a smarter way to figure out what course an item is in
|
||||
for course in modulestore().get_courses():
|
||||
if (course.location.org == item.location.org and
|
||||
course.location.course == item.location.course):
|
||||
break
|
||||
|
||||
lms_link = get_lms_link_for_item(item.location)
|
||||
lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
|
||||
|
||||
component_templates = defaultdict(list)
|
||||
|
||||
@@ -454,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, request.POST)
|
||||
|
||||
except NotFoundError:
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404
|
||||
|
||||
except ProcessingError:
|
||||
log.warning("Module raised an error while processing AJAX request",
|
||||
exc_info=True)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
@@ -1279,15 +1271,48 @@ def course_advanced_updates(request, org, course, name):
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
real_method = get_request_method(request)
|
||||
|
||||
|
||||
if real_method == 'GET':
|
||||
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
|
||||
elif real_method == 'DELETE':
|
||||
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json")
|
||||
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
|
||||
mimetype="application/json")
|
||||
elif real_method == 'POST' or real_method == 'PUT':
|
||||
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
|
||||
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json")
|
||||
|
||||
request_body = json.loads(request.body)
|
||||
#Whether or not to filter the tabs key out of the settings metadata
|
||||
filter_tabs = True
|
||||
#Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
|
||||
#to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
|
||||
#module, and to remove it if they have removed the open ended elements.
|
||||
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
|
||||
#Check to see if the user instantiated any open ended components
|
||||
found_oe_type = False
|
||||
#Get the course so that we can scrape current tabs
|
||||
course_module = modulestore().get_item(location)
|
||||
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
|
||||
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
|
||||
#Add an open ended tab to the course if needed
|
||||
changed, new_tabs = add_open_ended_panel_tab(course_module)
|
||||
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
|
||||
if changed:
|
||||
request_body.update({'tabs': new_tabs})
|
||||
#Indicate that tabs should not be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
#Set this flag to avoid the open ended tab removal code below.
|
||||
found_oe_type = True
|
||||
break
|
||||
#If we did not find an open ended module type in the advanced settings,
|
||||
# we may need to remove the open ended tab from the course.
|
||||
if not found_oe_type:
|
||||
#Remove open ended tab to the course if needed
|
||||
changed, new_tabs = remove_open_ended_panel_tab(course_module)
|
||||
if changed:
|
||||
request_body.update({'tabs': new_tabs})
|
||||
#Indicate that tabs should not be filtered out of the metadata
|
||||
filter_tabs = False
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xblock.core import Scope
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
import copy
|
||||
|
||||
class CourseMetadata(object):
|
||||
'''
|
||||
@@ -39,7 +39,7 @@ class CourseMetadata(object):
|
||||
return course
|
||||
|
||||
@classmethod
|
||||
def update_from_json(cls, course_location, jsondict):
|
||||
def update_from_json(cls, course_location, jsondict, filter_tabs=True):
|
||||
"""
|
||||
Decode the json into CourseMetadata and save any changed attrs to the db.
|
||||
|
||||
@@ -48,10 +48,16 @@ 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
|
||||
if not filter_tabs:
|
||||
filtered_list.remove("tabs")
|
||||
|
||||
for k, v in jsondict.iteritems():
|
||||
# should it be an error if one of the filtered list items is in the payload?
|
||||
if k in cls.FILTERED_LIST:
|
||||
if k in filtered_list:
|
||||
continue
|
||||
|
||||
if hasattr(descriptor, k) and getattr(descriptor, k) != v:
|
||||
|
||||
@@ -142,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = {
|
||||
|
||||
# To see stacktraces for MongoDB queries, set this to True.
|
||||
# Stacktraces slow down page loads drastically (for pages with lots of queries).
|
||||
DEBUG_TOOLBAR_MONGO_STACKTRACES = False
|
||||
DEBUG_TOOLBAR_MONGO_STACKTRACES = True
|
||||
|
||||
@@ -58,6 +58,10 @@ MODULESTORE = {
|
||||
'direct': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
'draft': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ class CmsNamespace(Namespace):
|
||||
"""
|
||||
Namespace with fields common to all blocks in Studio
|
||||
"""
|
||||
is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
|
||||
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
|
||||
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
|
||||
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
|
||||
|
||||
@@ -118,7 +118,7 @@ class LoncapaProblem(object):
|
||||
# 3. Assign from the OS's random number generator
|
||||
self.seed = state.get('seed', seed)
|
||||
if self.seed is None:
|
||||
self.seed = struct.unpack('i', os.urandom(4))
|
||||
self.seed = struct.unpack('i', os.urandom(4))[0]
|
||||
self.student_answers = state.get('student_answers', {})
|
||||
if 'correct_map' in state:
|
||||
self.correct_map.set_dict(state['correct_map'])
|
||||
|
||||
@@ -80,16 +80,17 @@ class CorrectMap(object):
|
||||
|
||||
Special migration case:
|
||||
If correct_map is a one-level dict, then convert it to the new dict of dicts format.
|
||||
'''
|
||||
if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict):
|
||||
# empty current dict
|
||||
self.__init__()
|
||||
|
||||
# create new dict entries
|
||||
'''
|
||||
# empty current dict
|
||||
self.__init__()
|
||||
|
||||
# create new dict entries
|
||||
if correct_map and not isinstance(correct_map.values()[0], dict):
|
||||
# special migration
|
||||
for k in correct_map:
|
||||
self.set(k, correct_map[k])
|
||||
self.set(k, correctness=correct_map[k])
|
||||
else:
|
||||
self.__init__()
|
||||
for k in correct_map:
|
||||
self.set(k, **correct_map[k])
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import logging
|
||||
import numbers
|
||||
import numpy
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import re
|
||||
import requests
|
||||
@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception):
|
||||
|
||||
class ResponseError(Exception):
|
||||
'''
|
||||
Error for failure in processing a response
|
||||
Error for failure in processing a response, including
|
||||
exceptions that occur when executing a custom script.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class StudentInputError(Exception):
|
||||
'''
|
||||
Error for an invalid student input.
|
||||
For example, submitting a string when the problem expects a number
|
||||
'''
|
||||
pass
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse):
|
||||
import sys
|
||||
type, value, traceback = sys.exc_info()
|
||||
|
||||
raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" %
|
||||
raise StudentInputError, ("Could not interpret '%s' as a number" %
|
||||
cgi.escape(student_answer)), traceback
|
||||
|
||||
if correct:
|
||||
@@ -1072,13 +1078,10 @@ def sympy_check2():
|
||||
correct = self.context['correct']
|
||||
messages = self.context['messages']
|
||||
overall_message = self.context['overall_message']
|
||||
|
||||
except Exception as err:
|
||||
print "oops in customresponse (code) error %s" % err
|
||||
print "context = ", self.context
|
||||
print traceback.format_exc()
|
||||
# Notify student
|
||||
raise StudentInputError(
|
||||
"Error: Problem could not be evaluated with your input")
|
||||
self._handle_exec_exception(err)
|
||||
|
||||
else:
|
||||
# self.code is not a string; assume its a function
|
||||
|
||||
@@ -1105,13 +1108,9 @@ def sympy_check2():
|
||||
nargs, args, kwargs))
|
||||
|
||||
ret = fn(*args[:nargs], **kwargs)
|
||||
|
||||
except Exception as err:
|
||||
log.error("oops in customresponse (cfn) error %s" % err)
|
||||
# print "context = ",self.context
|
||||
log.error(traceback.format_exc())
|
||||
raise Exception("oops in customresponse (cfn) error %s" % err)
|
||||
log.debug(
|
||||
"[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
|
||||
self._handle_exec_exception(err)
|
||||
|
||||
if type(ret) == dict:
|
||||
|
||||
@@ -1147,9 +1146,9 @@ def sympy_check2():
|
||||
correct = []
|
||||
messages = []
|
||||
for input_dict in input_list:
|
||||
correct.append('correct'
|
||||
correct.append('correct'
|
||||
if input_dict['ok'] else 'incorrect')
|
||||
msg = (self.clean_message_html(input_dict['msg'])
|
||||
msg = (self.clean_message_html(input_dict['msg'])
|
||||
if 'msg' in input_dict else None)
|
||||
messages.append(msg)
|
||||
|
||||
@@ -1157,7 +1156,7 @@ def sympy_check2():
|
||||
# Raise an exception
|
||||
else:
|
||||
log.error(traceback.format_exc())
|
||||
raise Exception(
|
||||
raise ResponseError(
|
||||
"CustomResponse: check function returned an invalid dict")
|
||||
|
||||
# The check function can return a boolean value,
|
||||
@@ -1174,7 +1173,7 @@ def sympy_check2():
|
||||
correct_map.set_overall_message(overall_message)
|
||||
|
||||
for k in range(len(idset)):
|
||||
npoints = (self.maxpoints[idset[k]]
|
||||
npoints = (self.maxpoints[idset[k]]
|
||||
if correct[k] == 'correct' else 0)
|
||||
correct_map.set(idset[k], correct[k], msg=messages[k],
|
||||
npoints=npoints)
|
||||
@@ -1227,6 +1226,22 @@ def sympy_check2():
|
||||
return {self.answer_ids[0]: self.expect}
|
||||
return self.default_answer_map
|
||||
|
||||
def _handle_exec_exception(self, err):
|
||||
'''
|
||||
Handle an exception raised during the execution of
|
||||
custom Python code.
|
||||
|
||||
Raises a ResponseError
|
||||
'''
|
||||
|
||||
# Log the error if we are debugging
|
||||
msg = 'Error occurred while evaluating CustomResponse'
|
||||
log.warning(msg, exc_info=True)
|
||||
|
||||
# Notify student with a student input error
|
||||
_, _, traceback_obj = sys.exc_info()
|
||||
raise ResponseError, err.message, traceback_obj
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse):
|
||||
submission = [json.loads(student_answers[
|
||||
k]) for k in sorted(self.answer_ids)]
|
||||
self.context.update({'submission': submission})
|
||||
exec self.code in global_context, self.context
|
||||
|
||||
try:
|
||||
exec self.code in global_context, self.context
|
||||
|
||||
except Exception as err:
|
||||
_, _, traceback_obj = sys.exc_info()
|
||||
raise ResponseError, ResponseError(err.message), traceback_obj
|
||||
|
||||
cmap = CorrectMap()
|
||||
cmap.set_dict(dict(zip(sorted(
|
||||
self.answer_ids), self.context['correct'])))
|
||||
@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse):
|
||||
option_scoring = dict([(option['id'], {
|
||||
'correctness': choices.get(option['choice']),
|
||||
'points': scoring.get(option['choice'])
|
||||
}) for option in self._find_options(inputfield) ])
|
||||
}) for option in self._find_options(inputfield)])
|
||||
|
||||
scoring_map[inputfield.get('id')] = option_scoring
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
% for choice_id, choice_description in choices:
|
||||
<label for="input_${id}_${choice_id}"
|
||||
% if input_type == 'radio' and choice_id == value:
|
||||
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
|
||||
<%
|
||||
if status == 'correct':
|
||||
correctness = 'correct'
|
||||
@@ -30,9 +30,9 @@
|
||||
class="choicegroup_${correctness}"
|
||||
% endif
|
||||
% endif
|
||||
>
|
||||
>
|
||||
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
|
||||
% if input_type == 'radio' and choice_id == value:
|
||||
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
|
||||
checked="true"
|
||||
% elif input_type != 'radio' and choice_id in value:
|
||||
checked="true"
|
||||
|
||||
@@ -13,6 +13,8 @@ import textwrap
|
||||
from . import test_system
|
||||
|
||||
import capa.capa_problem as lcp
|
||||
from capa.responsetypes import LoncapaProblemError, \
|
||||
StudentInputError, ResponseError
|
||||
from capa.correctmap import CorrectMap
|
||||
from capa.util import convert_files_to_filenames
|
||||
from capa.xqueue_interface import dateformat
|
||||
@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest):
|
||||
# Message is interpreted as an "overall message"
|
||||
self.assertEqual(correct_map.get_overall_message(), 'Message text')
|
||||
|
||||
def test_script_exception(self):
|
||||
def test_script_exception_function(self):
|
||||
|
||||
# Construct a script that will raise an exception
|
||||
script = textwrap.dedent("""
|
||||
@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest):
|
||||
problem = self.build_problem(script=script, cfn="check_func")
|
||||
|
||||
# Expect that an exception gets raised when we check the answer
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(ResponseError):
|
||||
problem.grade_answers({'1_2_1': '42'})
|
||||
|
||||
def test_script_exception_inline(self):
|
||||
|
||||
# Construct a script that will raise an exception
|
||||
script = 'raise Exception("Test")'
|
||||
problem = self.build_problem(answer=script)
|
||||
|
||||
# Expect that an exception gets raised when we check the answer
|
||||
with self.assertRaises(ResponseError):
|
||||
problem.grade_answers({'1_2_1': '42'})
|
||||
|
||||
def test_invalid_dict_exception(self):
|
||||
@@ -889,7 +901,7 @@ class CustomResponseTest(ResponseTest):
|
||||
problem = self.build_problem(script=script, cfn="check_func")
|
||||
|
||||
# Expect that an exception gets raised when we check the answer
|
||||
with self.assertRaises(Exception):
|
||||
with self.assertRaises(ResponseError):
|
||||
problem.grade_answers({'1_2_1': '42'})
|
||||
|
||||
|
||||
@@ -922,6 +934,18 @@ class SchematicResponseTest(ResponseTest):
|
||||
# is what we expect)
|
||||
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
|
||||
|
||||
def test_script_exception(self):
|
||||
|
||||
# Construct a script that will raise an exception
|
||||
script = "raise Exception('test')"
|
||||
problem = self.build_problem(answer=script)
|
||||
|
||||
# Expect that an exception gets raised when we check the answer
|
||||
with self.assertRaises(ResponseError):
|
||||
submission_dict = {'test': 'test'}
|
||||
input_dict = {'1_2_1': json.dumps(submission_dict)}
|
||||
problem.grade_answers(input_dict)
|
||||
|
||||
|
||||
class AnnotationResponseTest(ResponseTest):
|
||||
from response_xml_factory import AnnotationResponseXMLFactory
|
||||
|
||||
@@ -14,7 +14,7 @@ from capa.util import convert_files_to_filenames
|
||||
from .progress import Progress
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xblock.core import Integer, Scope, String, Boolean, Object, Float
|
||||
from .fields import Timedelta, Date
|
||||
from xmodule.util.date_utils import time_to_datetime
|
||||
@@ -91,7 +91,7 @@ class CapaFields(object):
|
||||
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state)
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
@@ -449,7 +449,14 @@ class CapaModule(CapaFields, XModule):
|
||||
return 'Error'
|
||||
|
||||
before = self.get_progress()
|
||||
d = handlers[dispatch](get)
|
||||
|
||||
try:
|
||||
d = handlers[dispatch](get)
|
||||
|
||||
except Exception as err:
|
||||
_, _, traceback_obj = sys.exc_info()
|
||||
raise ProcessingError, err.message, traceback_obj
|
||||
|
||||
after = self.get_progress()
|
||||
d.update({
|
||||
'progress_changed': after != before,
|
||||
@@ -571,7 +578,7 @@ class CapaModule(CapaFields, XModule):
|
||||
# save any state changes that may occur
|
||||
self.set_state_from_lcp()
|
||||
return response
|
||||
|
||||
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
@@ -720,9 +727,24 @@ class CapaModule(CapaFields, XModule):
|
||||
try:
|
||||
correct_map = self.lcp.grade_answers(answers)
|
||||
self.set_state_from_lcp()
|
||||
except StudentInputError as inst:
|
||||
log.exception("StudentInputError in capa_module:problem_check")
|
||||
return {'success': inst.message}
|
||||
|
||||
except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
|
||||
log.warning("StudentInputError in capa_module:problem_check",
|
||||
exc_info=True)
|
||||
|
||||
# If the user is a staff member, include
|
||||
# the full exception, including traceback,
|
||||
# in the response
|
||||
if self.system.user_is_staff:
|
||||
msg = "Staff debug info: %s" % traceback.format_exc()
|
||||
|
||||
# Otherwise, display just an error message,
|
||||
# without a stack trace
|
||||
else:
|
||||
msg = "Error: %s" % str(inst.message)
|
||||
|
||||
return {'success': msg}
|
||||
|
||||
except Exception, err:
|
||||
if self.system.DEBUG:
|
||||
msg = "Error checking problem: " + str(err)
|
||||
@@ -773,7 +795,7 @@ class CapaModule(CapaFields, XModule):
|
||||
event_info['answers'] = answers
|
||||
|
||||
# Too late. Cannot submit
|
||||
if self.closed() and not self.max_attempts ==0:
|
||||
if self.closed() and not self.max_attempts == 0:
|
||||
event_info['failure'] = 'closed'
|
||||
self.system.track_function('save_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
@@ -793,7 +815,7 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
self.system.track_function('save_problem_success', event_info)
|
||||
msg = "Your answers have been saved"
|
||||
if not self.max_attempts ==0:
|
||||
if not self.max_attempts == 0:
|
||||
msg += " but not graded. Hit 'Check' to grade them."
|
||||
return {'success': True,
|
||||
'msg': msg}
|
||||
|
||||
@@ -9,11 +9,12 @@ from xblock.core import Integer, Scope, String, Boolean, List
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
from collections import namedtuple
|
||||
from .fields import Date
|
||||
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
|
||||
"skip_spelling_checks", "due", "graceperiod", "max_score"]
|
||||
"skip_spelling_checks", "due", "graceperiod"]
|
||||
|
||||
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
|
||||
"student_attempts", "ready_to_reset"]
|
||||
@@ -66,9 +67,9 @@ class CombinedOpenEndedFields(object):
|
||||
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
|
||||
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
|
||||
scope=Scope.settings)
|
||||
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
|
||||
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
|
||||
|
||||
class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
@@ -118,7 +119,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
Definition file should have one or many task blocks, a rubric block, and a prompt block:
|
||||
|
||||
Sample file:
|
||||
<combinedopenended attempts="10000" max_score="1">
|
||||
<combinedopenended attempts="10000">
|
||||
<rubric>
|
||||
Blah blah rubric.
|
||||
</rubric>
|
||||
@@ -190,8 +191,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
|
||||
def get_score(self):
|
||||
return self.child_module.get_score()
|
||||
|
||||
#def max_score(self):
|
||||
# return self.child_module.max_score()
|
||||
def max_score(self):
|
||||
return self.child_module.max_score()
|
||||
|
||||
def get_progress(self):
|
||||
return self.child_module.get_progress()
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
class InvalidDefinitionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
class ProcessingError(Exception):
|
||||
'''
|
||||
An error occurred while processing a request to the XModule.
|
||||
For example: if an exception occurs while checking a capa problem.
|
||||
'''
|
||||
pass
|
||||
|
||||
@@ -10,6 +10,7 @@ from collections import namedtuple
|
||||
|
||||
from .exceptions import InvalidLocationError, InsufficientSpecificationError
|
||||
from xmodule.errortracker import ErrorLog, make_error_tracker
|
||||
from bson.son import SON
|
||||
|
||||
log = logging.getLogger('mitx.' + 'modulestore')
|
||||
|
||||
@@ -457,3 +458,13 @@ class ModuleStoreBase(ModuleStore):
|
||||
if c.id == course_id:
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def namedtuple_to_son(namedtuple, prefix=''):
|
||||
"""
|
||||
Converts a namedtuple into a SON object with the same key order
|
||||
"""
|
||||
son = SON()
|
||||
for idx, field_name in enumerate(namedtuple._fields):
|
||||
son[prefix + field_name] = namedtuple[idx]
|
||||
return son
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from datetime import datetime
|
||||
|
||||
from . import ModuleStoreBase, Location
|
||||
from . import ModuleStoreBase, Location, namedtuple_to_son
|
||||
from .exceptions import ItemNotFoundError
|
||||
import logging
|
||||
|
||||
DRAFT = 'draft'
|
||||
|
||||
@@ -15,11 +16,11 @@ def as_draft(location):
|
||||
|
||||
def wrap_draft(item):
|
||||
"""
|
||||
Sets `item.cms.is_draft` to `True` if the item is a
|
||||
Sets `item.is_draft` to `True` if the item is a
|
||||
draft, and `False` otherwise. Sets the item's location to the
|
||||
non-draft location in either case
|
||||
"""
|
||||
item.cms.is_draft = item.location.revision == DRAFT
|
||||
setattr(item, 'is_draft', item.location.revision == DRAFT)
|
||||
item.location = item.location._replace(revision=None)
|
||||
return item
|
||||
|
||||
@@ -55,11 +56,10 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
|
||||
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
|
||||
try:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth=0))
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth=depth))
|
||||
except ItemNotFoundError:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(location, depth=0))
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item(location, depth=depth))
|
||||
|
||||
def get_instance(self, course_id, location, depth=0):
|
||||
"""
|
||||
@@ -67,11 +67,10 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
TODO (vshnayder): this may want to live outside the modulestore eventually
|
||||
"""
|
||||
|
||||
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
|
||||
try:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=0))
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=depth))
|
||||
except ItemNotFoundError:
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=0))
|
||||
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=depth))
|
||||
|
||||
def get_items(self, location, course_id=None, depth=0):
|
||||
"""
|
||||
@@ -88,9 +87,8 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
draft_loc = as_draft(location)
|
||||
|
||||
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
|
||||
draft_items = super(DraftModuleStore, self).get_items(draft_loc, course_id=course_id, depth=0)
|
||||
items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=0)
|
||||
draft_items = super(DraftModuleStore, self).get_items(draft_loc, course_id=course_id, depth=depth)
|
||||
items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=depth)
|
||||
|
||||
draft_locs_found = set(item.location._replace(revision=None) for item in draft_items)
|
||||
non_draft_items = [
|
||||
@@ -118,7 +116,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if not draft_item.cms.is_draft:
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
return super(DraftModuleStore, self).update_item(draft_loc, data)
|
||||
@@ -133,7 +131,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
if not draft_item.cms.is_draft:
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
return super(DraftModuleStore, self).update_children(draft_loc, children)
|
||||
@@ -149,7 +147,7 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
draft_loc = as_draft(location)
|
||||
draft_item = self.get_item(location)
|
||||
|
||||
if not draft_item.cms.is_draft:
|
||||
if not getattr(draft_item, 'is_draft', False):
|
||||
self.clone_item(location, draft_loc)
|
||||
|
||||
if 'is_draft' in metadata:
|
||||
@@ -192,3 +190,36 @@ class DraftModuleStore(ModuleStoreBase):
|
||||
"""
|
||||
super(DraftModuleStore, self).clone_item(location, as_draft(location))
|
||||
super(DraftModuleStore, self).delete_item(location)
|
||||
|
||||
def _query_children_for_cache_children(self, items):
|
||||
# first get non-draft in a round-trip
|
||||
queried_children = []
|
||||
to_process_non_drafts = super(DraftModuleStore, self)._query_children_for_cache_children(items)
|
||||
|
||||
to_process_dict = {}
|
||||
for non_draft in to_process_non_drafts:
|
||||
to_process_dict[Location(non_draft["_id"])] = non_draft
|
||||
|
||||
# now query all draft content in another round-trip
|
||||
query = {
|
||||
'_id': {'$in': [namedtuple_to_son(as_draft(Location(item))) for item in items]}
|
||||
}
|
||||
to_process_drafts = list(self.collection.find(query))
|
||||
|
||||
# now we have to go through all drafts and replace the non-draft
|
||||
# with the draft. This is because the semantics of the DraftStore is to
|
||||
# always return the draft - if available
|
||||
for draft in to_process_drafts:
|
||||
draft_loc = Location(draft["_id"])
|
||||
draft_as_non_draft_loc = draft_loc._replace(revision=None)
|
||||
|
||||
# does non-draft exist in the collection
|
||||
# if so, replace it
|
||||
if draft_as_non_draft_loc in to_process_dict:
|
||||
to_process_dict[draft_as_non_draft_loc] = draft
|
||||
|
||||
# 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)
|
||||
|
||||
return queried_children
|
||||
|
||||
@@ -3,7 +3,6 @@ import sys
|
||||
import logging
|
||||
import copy
|
||||
|
||||
from bson.son import SON
|
||||
from collections import namedtuple
|
||||
from fs.osfs import OSFS
|
||||
from itertools import repeat
|
||||
@@ -19,7 +18,7 @@ from xmodule.error_module import ErrorDescriptor
|
||||
from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError
|
||||
from xblock.core import Scope
|
||||
|
||||
from . import ModuleStoreBase, Location
|
||||
from . import ModuleStoreBase, Location, namedtuple_to_son
|
||||
from .draft import DraftModuleStore
|
||||
from .exceptions import (ItemNotFoundError,
|
||||
DuplicateItemError)
|
||||
@@ -202,16 +201,6 @@ def location_to_query(location, wildcard=True):
|
||||
return query
|
||||
|
||||
|
||||
def namedtuple_to_son(ntuple, prefix=''):
|
||||
"""
|
||||
Converts a namedtuple into a SON object with the same key order
|
||||
"""
|
||||
son = SON()
|
||||
for idx, field_name in enumerate(ntuple._fields):
|
||||
son[prefix + field_name] = ntuple[idx]
|
||||
return son
|
||||
|
||||
|
||||
metadata_cache_key = attrgetter('org', 'course')
|
||||
|
||||
|
||||
@@ -383,6 +372,13 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
item['location'] = item['_id']
|
||||
del item['_id']
|
||||
|
||||
def _query_children_for_cache_children(self, items):
|
||||
# first get non-draft in a round-trip
|
||||
query = {
|
||||
'_id': {'$in': [namedtuple_to_son(Location(item)) for item in items]}
|
||||
}
|
||||
return list(self.collection.find(query))
|
||||
|
||||
def _cache_children(self, items, depth=0):
|
||||
"""
|
||||
Returns a dictionary mapping Location -> item data, populated with json data
|
||||
@@ -407,13 +403,10 @@ class MongoModuleStore(ModuleStoreBase):
|
||||
# Load all children by id. See
|
||||
# http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or
|
||||
# for or-query syntax
|
||||
to_process = []
|
||||
if children:
|
||||
query = {
|
||||
'_id': {'$in': [namedtuple_to_son(Location(child)) for child in children]}
|
||||
}
|
||||
to_process = self.collection.find(query)
|
||||
else:
|
||||
to_process = []
|
||||
to_process = self._query_children_for_cache_children(children)
|
||||
|
||||
# If depth is None, then we just recurse until we hit all the descendents
|
||||
if depth is not None:
|
||||
depth -= 1
|
||||
|
||||
@@ -136,3 +136,4 @@ def delete_course(modulestore, contentstore, source_location, commit = False):
|
||||
modulestore.delete_item(source_location)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -356,6 +356,26 @@ def remap_namespace(module, target_location_namespace):
|
||||
|
||||
return module
|
||||
|
||||
def validate_no_non_editable_metadata(module_store, course_id, category, allowed=[]):
|
||||
'''
|
||||
Assert that there is no metadata within a particular category that we can't support editing
|
||||
However we always allow display_name and 'xml_attribtues'
|
||||
'''
|
||||
allowed = allowed + ['xml_attributes', 'display_name']
|
||||
|
||||
err_cnt = 0
|
||||
for module_loc in module_store.modules[course_id]:
|
||||
module = module_store.modules[course_id][module_loc]
|
||||
if module.location.category == category:
|
||||
my_metadata = dict(own_metadata(module))
|
||||
for key in my_metadata.keys():
|
||||
if key not in allowed:
|
||||
err_cnt = err_cnt + 1
|
||||
print ': found metadata on {0}. Studio will not support editing this piece of metadata, so it is not allowed. Metadata: {1} = {2}'. format(module.location.url(), key, my_metadata[key])
|
||||
|
||||
return err_cnt
|
||||
|
||||
|
||||
def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category):
|
||||
err_cnt = 0
|
||||
|
||||
@@ -440,6 +460,13 @@ def perform_xlint(data_dir, course_dirs,
|
||||
err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential")
|
||||
# constrain that sequentials only have 'verticals'
|
||||
err_cnt += validate_category_hierarchy(module_store, course_id, "sequential", "vertical")
|
||||
# don't allow metadata on verticals, since we can't edit them in studio
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "vertical")
|
||||
# don't allow metadata on chapters, since we can't edit them in studio
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "chapter",['start'])
|
||||
# don't allow metadata on sequences that we can't edit
|
||||
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "sequential",
|
||||
['due','format','start','graded'])
|
||||
|
||||
# check for a presence of a course marketing video
|
||||
location_elements = course_id.split('/')
|
||||
@@ -456,3 +483,5 @@ def perform_xlint(data_dir, course_dirs,
|
||||
print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing"
|
||||
else:
|
||||
print "This course can be imported successfully."
|
||||
|
||||
return err_cnt
|
||||
|
||||
@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware")
|
||||
# attempts specified in xml definition overrides this.
|
||||
MAX_ATTEMPTS = 1
|
||||
|
||||
# Set maximum available number of points.
|
||||
# Overriden by max_score specified in xml.
|
||||
MAX_SCORE = 1
|
||||
|
||||
#The highest score allowed for the overall xmodule and for each rubric point
|
||||
MAX_SCORE_ALLOWED = 50
|
||||
|
||||
@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module():
|
||||
Definition file should have one or many task blocks, a rubric block, and a prompt block:
|
||||
|
||||
Sample file:
|
||||
<combinedopenended attempts="10000" max_score="1">
|
||||
<combinedopenended attempts="10000">
|
||||
<rubric>
|
||||
Blah blah rubric.
|
||||
</rubric>
|
||||
@@ -153,13 +149,9 @@ class CombinedOpenEndedV1Module():
|
||||
raise
|
||||
self.display_due_date = self.timeinfo.display_due_date
|
||||
|
||||
# Used for progress / grading. Currently get credit just for
|
||||
# completion (doesn't matter if you self-assessed correct/incorrect).
|
||||
self._max_score = self.instance_state.get('max_score', MAX_SCORE)
|
||||
|
||||
self.rubric_renderer = CombinedOpenEndedRubric(system, True)
|
||||
rubric_string = stringify_children(definition['rubric'])
|
||||
self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED, self._max_score)
|
||||
self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED)
|
||||
|
||||
#Static data is passed to the child modules to render
|
||||
self.static_data = {
|
||||
|
||||
@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object):
|
||||
raise RubricParsingError(error_message)
|
||||
return {'success': success, 'html': html, 'rubric_scores': rubric_scores}
|
||||
|
||||
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score):
|
||||
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed):
|
||||
rubric_dict = self.render_rubric(rubric_string)
|
||||
success = rubric_dict['success']
|
||||
rubric_feedback = rubric_dict['html']
|
||||
@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object):
|
||||
log.error(error_message)
|
||||
raise RubricParsingError(error_message)
|
||||
|
||||
if int(total) != int(max_score):
|
||||
#This is a staff_facing_error
|
||||
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}. Contact the learning sciences group for assistance.".format(
|
||||
max_score, location, total)
|
||||
log.error(error_msg)
|
||||
raise RubricParsingError(error_msg)
|
||||
return int(total)
|
||||
|
||||
def extract_categories(self, element):
|
||||
'''
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from xblock.core import Integer, Float
|
||||
|
||||
|
||||
class StringyFloat(Float):
|
||||
"""
|
||||
A model type that converts from string to floats when reading from json
|
||||
"""
|
||||
|
||||
def from_json(self, value):
|
||||
try:
|
||||
return float(value)
|
||||
except:
|
||||
return None
|
||||
|
||||
@@ -11,7 +11,7 @@ from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from .timeinfo import TimeInfo
|
||||
from xblock.core import Object, Integer, Boolean, String, Scope
|
||||
from .fields import Date
|
||||
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
|
||||
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
|
||||
@@ -27,13 +27,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
|
||||
|
||||
|
||||
class PeerGradingFields(object):
|
||||
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.", default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
|
||||
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, scope=Scope.settings)
|
||||
is_graded = Boolean(help="Whether or not this module is scored.",default=IS_GRADED, scope=Scope.settings)
|
||||
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
|
||||
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
|
||||
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
|
||||
scope=Scope.settings)
|
||||
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
|
||||
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
|
||||
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
|
||||
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE, scope=Scope.settings)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),scope=Scope.student_state)
|
||||
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),
|
||||
scope=Scope.student_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
|
||||
|
||||
class PeerGradingModule(PeerGradingFields, XModule):
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
metadata:
|
||||
display_name: Open Ended Response
|
||||
max_attempts: 1
|
||||
max_score: 1
|
||||
is_graded: False
|
||||
version: 1
|
||||
display_name: Open Ended Response
|
||||
skip_spelling_checks: False
|
||||
accept_file_upload: False
|
||||
weight: ""
|
||||
data: |
|
||||
<combinedopenended>
|
||||
<rubric>
|
||||
|
||||
@@ -6,6 +6,7 @@ metadata:
|
||||
link_to_location: None
|
||||
is_graded: False
|
||||
max_grade: 1
|
||||
weight: ""
|
||||
data: |
|
||||
<peergrading>
|
||||
</peergrading>
|
||||
|
||||
@@ -7,6 +7,8 @@ import random
|
||||
|
||||
import xmodule
|
||||
import capa
|
||||
from capa.responsetypes import StudentInputError, \
|
||||
LoncapaProblemError, ResponseError
|
||||
from xmodule.capa_module import CapaModule
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
@@ -407,7 +409,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_html.return_value = "Test HTML"
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
@@ -428,7 +430,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_is_correct.return_value = False
|
||||
|
||||
# Check the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '0'}
|
||||
get_request_dict = {CapaFactory.input_key(): '0'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that the problem is marked correct
|
||||
@@ -446,7 +448,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
|
||||
mock_closed.return_value = True
|
||||
with self.assertRaises(xmodule.exceptions.NotFoundError):
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
module.check_problem(get_request_dict)
|
||||
|
||||
# Expect that number of attempts NOT incremented
|
||||
@@ -492,7 +494,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_is_queued.return_value = True
|
||||
mock_get_queuetime.return_value = datetime.datetime.now()
|
||||
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
@@ -502,21 +504,61 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_check_problem_student_input_error(self):
|
||||
module = CapaFactory.create(attempts=1)
|
||||
def test_check_problem_error(self):
|
||||
|
||||
# Simulate a student input exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
||||
mock_grade.side_effect = capa.responsetypes.StudentInputError('test error')
|
||||
# Try each exception that capa_module should handle
|
||||
for exception_class in [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
ResponseError]:
|
||||
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
# Create the module
|
||||
module = CapaFactory.create(attempts=1)
|
||||
|
||||
# Ensure that the user is NOT staff
|
||||
module.system.user_is_staff = False
|
||||
|
||||
# Simulate answering a problem that raises the exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
||||
mock_grade.side_effect = exception_class('test error')
|
||||
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
expected_msg = 'Error: test error'
|
||||
self.assertEqual(expected_msg, result['success'])
|
||||
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_check_problem_error_with_staff_user(self):
|
||||
|
||||
# Try each exception that capa module should handle
|
||||
for exception_class in [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
ResponseError]:
|
||||
|
||||
# Create the module
|
||||
module = CapaFactory.create(attempts=1)
|
||||
|
||||
# Ensure that the user IS staff
|
||||
module.system.user_is_staff = True
|
||||
|
||||
# Simulate answering a problem that raises an exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
||||
mock_grade.side_effect = exception_class('test error')
|
||||
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
self.assertTrue('test error' in result['success'])
|
||||
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
# We DO include traceback information for staff users
|
||||
self.assertTrue('Traceback' in result['success'])
|
||||
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
|
||||
def test_reset_problem(self):
|
||||
@@ -573,11 +615,11 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(done=False)
|
||||
|
||||
# Save the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that answers are saved to the problem
|
||||
expected_answers = { CapaFactory.answer_key(): '3.14'}
|
||||
expected_answers = {CapaFactory.answer_key(): '3.14'}
|
||||
self.assertEqual(module.lcp.student_answers, expected_answers)
|
||||
|
||||
# Expect that the result is success
|
||||
@@ -592,7 +634,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
mock_closed.return_value = True
|
||||
|
||||
# Try to save the problem
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that the result is failure
|
||||
@@ -603,7 +645,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(rerandomize='always', done=True)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we cannot save
|
||||
@@ -614,7 +656,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
module = CapaFactory.create(rerandomize='never', done=True)
|
||||
|
||||
# Try to save
|
||||
get_request_dict = { CapaFactory.input_key(): '3.14'}
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.save_problem(get_request_dict)
|
||||
|
||||
# Expect that we succeed
|
||||
@@ -626,7 +668,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Just in case, we also check what happens if we have
|
||||
# more attempts than allowed.
|
||||
attempts = random.randint(1, 10)
|
||||
module = CapaFactory.create(attempts=attempts -1, max_attempts=attempts)
|
||||
module = CapaFactory.create(attempts=attempts - 1, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Final Check")
|
||||
|
||||
module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
|
||||
@@ -636,14 +678,14 @@ class CapaModuleTest(unittest.TestCase):
|
||||
self.assertEqual(module.check_button_name(), "Final Check")
|
||||
|
||||
# Otherwise, button name is "Check"
|
||||
module = CapaFactory.create(attempts=attempts -2, max_attempts=attempts)
|
||||
module = CapaFactory.create(attempts=attempts - 2, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Check")
|
||||
|
||||
module = CapaFactory.create(attempts=attempts -3, max_attempts=attempts)
|
||||
module = CapaFactory.create(attempts=attempts - 3, max_attempts=attempts)
|
||||
self.assertEqual(module.check_button_name(), "Check")
|
||||
|
||||
# If no limit on attempts, then always show "Check"
|
||||
module = CapaFactory.create(attempts=attempts -3)
|
||||
module = CapaFactory.create(attempts=attempts - 3)
|
||||
self.assertEqual(module.check_button_name(), "Check")
|
||||
|
||||
module = CapaFactory.create(attempts=0)
|
||||
|
||||
@@ -5,11 +5,15 @@ import unittest
|
||||
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
|
||||
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
|
||||
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
import capa.xqueue_interface as xqueue_interface
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import test_system
|
||||
|
||||
@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.openendedchild = OpenEndedChild(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
|
||||
def test_latest_answer_empty(self):
|
||||
@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
self.test_system.location = self.location
|
||||
self.mock_xqueue = MagicMock()
|
||||
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
|
||||
|
||||
def constructed_callback(dispatch="score_update"):
|
||||
return dispatch
|
||||
|
||||
self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue',
|
||||
|
||||
self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback,
|
||||
'default_queuename': 'testqueue',
|
||||
'waittime': 1}
|
||||
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "open_ended", "combinedopenended",
|
||||
"SampleQuestion"])
|
||||
|
||||
definition_template = """
|
||||
<combinedopenended attempts="10000">
|
||||
{rubric}
|
||||
{prompt}
|
||||
<task>
|
||||
{task1}
|
||||
</task>
|
||||
<task>
|
||||
{task2}
|
||||
</task>
|
||||
</combinedopenended>
|
||||
"""
|
||||
prompt = "<prompt>This is a question prompt</prompt>"
|
||||
rubric = '''<rubric><rubric>
|
||||
<category>
|
||||
@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
</openendedparam>
|
||||
</openended>'''
|
||||
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
|
||||
descriptor = Mock()
|
||||
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
|
||||
descriptor = Mock(data=full_definition)
|
||||
test_system = test_system()
|
||||
combinedoe_container = CombinedOpenEndedModule(test_system,
|
||||
location,
|
||||
descriptor,
|
||||
model_data={'data': full_definition, 'weight' : '1'})
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
# TODO: this constructor call is definitely wrong, but neither branch
|
||||
# of the merge matches the module constructor. Someone (Vik?) should fix this.
|
||||
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
|
||||
@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
changed = self.combinedoe.update_task_states()
|
||||
|
||||
self.assertTrue(changed)
|
||||
|
||||
def test_get_max_score(self):
|
||||
changed = self.combinedoe.update_task_states()
|
||||
self.combinedoe.state = "done"
|
||||
self.combinedoe.is_scored = True
|
||||
max_score = self.combinedoe.max_score()
|
||||
self.assertEqual(max_score, 1)
|
||||
|
||||
def test_container_get_max_score(self):
|
||||
#The progress view requires that this function be exposed
|
||||
max_score = self.combinedoe_container.max_score()
|
||||
self.assertEqual(max_score, None)
|
||||
|
||||
def test_container_weight(self):
|
||||
weight = self.combinedoe_container.weight
|
||||
self.assertEqual(weight,1)
|
||||
|
||||
@@ -43,13 +43,15 @@ rake pep8 > pep8.log || cat pep8.log
|
||||
rake pylint > pylint.log || cat pylint.log
|
||||
|
||||
TESTS_FAILED=0
|
||||
|
||||
# Run the python unit tests
|
||||
rake test_cms[false] || TESTS_FAILED=1
|
||||
rake test_lms[false] || TESTS_FAILED=1
|
||||
rake test_common/lib/capa || TESTS_FAILED=1
|
||||
rake test_common/lib/xmodule || TESTS_FAILED=1
|
||||
# Don't run the lms jasmine tests for now because
|
||||
# they mostly all fail anyhow
|
||||
# rake phantomjs_jasmine_lms || true
|
||||
|
||||
# Run the jaavascript unit tests
|
||||
rake phantomjs_jasmine_lms || TESTS_FAILED=1
|
||||
rake phantomjs_jasmine_cms || TESTS_FAILED=1
|
||||
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ import unittest
|
||||
import threading
|
||||
import json
|
||||
import urllib
|
||||
import urlparse
|
||||
import time
|
||||
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
|
||||
|
||||
class MockXQueueServerTest(unittest.TestCase):
|
||||
'''
|
||||
@@ -22,11 +23,16 @@ class MockXQueueServerTest(unittest.TestCase):
|
||||
|
||||
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 = 8034
|
||||
self.server_url = 'http://127.0.0.1:%d' % server_port
|
||||
self.server = MockXQueueServer(server_port,
|
||||
{'correct': True, 'score': 1, 'msg': ''})
|
||||
{'correct': True, 'score': 1, 'msg': ''})
|
||||
|
||||
# Start the server in a separate daemon thread
|
||||
server_thread = threading.Thread(target=self.server.serve_forever)
|
||||
@@ -48,18 +54,18 @@ class MockXQueueServerTest(unittest.TestCase):
|
||||
callback_url = 'http://127.0.0.1:8000/test_callback'
|
||||
|
||||
grade_header = json.dumps({'lms_callback_url': callback_url,
|
||||
'lms_key': 'test_queuekey',
|
||||
'queue_name': 'test_queue'})
|
||||
'lms_key': 'test_queuekey',
|
||||
'queue_name': 'test_queue'})
|
||||
|
||||
grade_body = json.dumps({'student_info': 'test',
|
||||
'grader_payload': 'test',
|
||||
'student_response': 'test'})
|
||||
|
||||
grade_request = {'xqueue_header': grade_header,
|
||||
'xqueue_body': grade_body}
|
||||
'xqueue_body': grade_body}
|
||||
|
||||
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
|
||||
urllib.urlencode(grade_request))
|
||||
urllib.urlencode(grade_request))
|
||||
|
||||
response_dict = json.loads(response_handle.read())
|
||||
|
||||
@@ -71,8 +77,8 @@ class MockXQueueServerTest(unittest.TestCase):
|
||||
|
||||
# Expect that the server tries to post back the grading info
|
||||
xqueue_body = json.dumps({'correct': True, 'score': 1,
|
||||
'msg': '<div></div>'})
|
||||
'msg': '<div></div>'})
|
||||
expected_callback_dict = {'xqueue_header': grade_header,
|
||||
'xqueue_body': xqueue_body}
|
||||
'xqueue_body': xqueue_body}
|
||||
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url,
|
||||
expected_callback_dict)
|
||||
expected_callback_dict)
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
@@ -23,7 +23,7 @@ from .models import StudentModule
|
||||
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
|
||||
from student.models import unique_id_for_user
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.exceptions import NotFoundError, ProcessingError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.x_module import ModuleSystem
|
||||
@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id):
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, p)
|
||||
|
||||
# If we can't find the module, respond with a 404
|
||||
except NotFoundError:
|
||||
log.exception("Module indicating to user that request doesn't exist")
|
||||
raise Http404
|
||||
|
||||
# For XModule-specific errors, we respond with 400
|
||||
except ProcessingError:
|
||||
log.warning("Module encountered an error while prcessing AJAX call",
|
||||
exc_info=True)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# If any other error occurred, re-raise it to trigger a 500 response
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
'''
|
||||
Test for lms courseware app
|
||||
'''
|
||||
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
|
||||
from django.contrib.auth.models import User, Group
|
||||
@@ -11,8 +17,6 @@ from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
|
||||
|
||||
# Need access to internal func to put users in the right group
|
||||
from courseware import grades
|
||||
@@ -29,6 +33,7 @@ from xmodule.modulestore.xml import XMLModuleStore
|
||||
|
||||
log = logging.getLogger("mitx." + __name__)
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
"""Parse response, which is assumed to be json"""
|
||||
return json.loads(response.content)
|
||||
@@ -47,7 +52,7 @@ def get_registration(email):
|
||||
def mongo_store_config(data_dir):
|
||||
'''
|
||||
Defines default module store using MongoModuleStore
|
||||
|
||||
|
||||
Use of this config requires mongo to be running
|
||||
'''
|
||||
return {
|
||||
@@ -101,7 +106,10 @@ TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
|
||||
|
||||
|
||||
class LoginEnrollmentTestCase(TestCase):
|
||||
'''Base TestCase providing support for user creation, activation, login, and course enrollment'''
|
||||
'''
|
||||
Base TestCase providing support for user creation,
|
||||
activation, login, and course enrollment
|
||||
'''
|
||||
|
||||
def assertRedirectsNoFollow(self, response, expected_url):
|
||||
"""
|
||||
@@ -112,22 +120,26 @@ class LoginEnrollmentTestCase(TestCase):
|
||||
Some of the code taken from django.test.testcases.py
|
||||
"""
|
||||
self.assertEqual(response.status_code, 302,
|
||||
'Response status code was {0} instead of 302'.format(response.status_code))
|
||||
'Response status code was %d instead of 302'
|
||||
% (response.status_code))
|
||||
url = response['Location']
|
||||
|
||||
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
|
||||
if not (e_scheme or e_netloc):
|
||||
expected_url = urlunsplit(('http', 'testserver', e_path, e_query, e_fragment))
|
||||
expected_url = urlunsplit(('http', 'testserver',
|
||||
e_path, e_query, e_fragment))
|
||||
|
||||
self.assertEqual(url, expected_url, "Response redirected to '{0}', expected '{1}'".format(
|
||||
url, expected_url))
|
||||
self.assertEqual(url, expected_url,
|
||||
"Response redirected to '%s', expected '%s'" %
|
||||
(url, expected_url))
|
||||
|
||||
def setup_viewtest_user(self):
|
||||
'''create a user account, activate, and log in'''
|
||||
self.viewtest_email = 'view@test.com'
|
||||
self.viewtest_password = 'foo'
|
||||
self.viewtest_username = 'viewtest'
|
||||
self.create_account(self.viewtest_username, self.viewtest_email, self.viewtest_password)
|
||||
self.create_account(self.viewtest_username,
|
||||
self.viewtest_email, self.viewtest_password)
|
||||
self.activate_user(self.viewtest_email)
|
||||
self.login(self.viewtest_email, self.viewtest_password)
|
||||
|
||||
@@ -185,7 +197,8 @@ class LoginEnrollmentTestCase(TestCase):
|
||||
activation_key = get_registration(email).activation_key
|
||||
|
||||
# and now we try to activate
|
||||
resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
|
||||
url = reverse('activate', kwargs={'key': activation_key})
|
||||
resp = self.client.get(url)
|
||||
return resp
|
||||
|
||||
def activate_user(self, email):
|
||||
@@ -205,7 +218,8 @@ class LoginEnrollmentTestCase(TestCase):
|
||||
def try_enroll(self, course):
|
||||
"""Try to enroll. Return bool success instead of asserting it."""
|
||||
data = self._enroll(course)
|
||||
print 'Enrollment in {0} result: {1}'.format(course.location.url(), data)
|
||||
print ('Enrollment in %s result: %s'
|
||||
% (course.location.url(), str(data)))
|
||||
return data['success']
|
||||
|
||||
def enroll(self, course):
|
||||
@@ -229,7 +243,8 @@ class LoginEnrollmentTestCase(TestCase):
|
||||
"""
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}".format(resp.status_code, url, code))
|
||||
"got code %d for url '%s'. Expected code %d"
|
||||
% (resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
def check_for_post_code(self, code, url, data={}):
|
||||
@@ -239,7 +254,8 @@ class LoginEnrollmentTestCase(TestCase):
|
||||
"""
|
||||
resp = self.client.post(url, data)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}".format(resp.status_code, url, code))
|
||||
"got code %d for url '%s'. Expected code %d"
|
||||
% (resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
|
||||
@@ -260,8 +276,10 @@ class ActivateLoginTest(LoginEnrollmentTestCase):
|
||||
class PageLoaderTestCase(LoginEnrollmentTestCase):
|
||||
''' Base class that adds a function to load all pages in a modulestore '''
|
||||
|
||||
def check_pages_load(self, module_store):
|
||||
"""Make all locations in course load"""
|
||||
def check_random_page_loads(self, module_store):
|
||||
'''
|
||||
Choose a page in the course randomly, and assert that it loads
|
||||
'''
|
||||
# enroll in the course before trying to access pages
|
||||
courses = module_store.get_courses()
|
||||
self.assertEqual(len(courses), 1)
|
||||
@@ -269,77 +287,71 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
|
||||
self.enroll(course)
|
||||
course_id = course.id
|
||||
|
||||
num = 0
|
||||
num_bad = 0
|
||||
all_ok = True
|
||||
# Search for items in the course
|
||||
# None is treated as a wildcard
|
||||
course_loc = course.location
|
||||
location_query = Location(course_loc.tag, course_loc.org,
|
||||
course_loc.course, None, None, None)
|
||||
|
||||
for descriptor in module_store.get_items(
|
||||
Location(None, None, None, None, None)):
|
||||
items = module_store.get_items(location_query)
|
||||
|
||||
num += 1
|
||||
print "Checking ", descriptor.location.url()
|
||||
if len(items) < 1:
|
||||
self.fail('Could not retrieve any items from course')
|
||||
else:
|
||||
descriptor = random.choice(items)
|
||||
|
||||
# We have ancillary course information now as modules and we can't simply use 'jump_to' to view them
|
||||
if descriptor.location.category == 'about':
|
||||
resp = self.client.get(reverse('about_course', kwargs={'course_id': course_id}))
|
||||
msg = str(resp.status_code)
|
||||
# We have ancillary course information now as modules
|
||||
# and we can't simply use 'jump_to' to view them
|
||||
if descriptor.location.category == 'about':
|
||||
self._assert_loads('about_course',
|
||||
{'course_id': course_id},
|
||||
descriptor)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'static_tab':
|
||||
resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug': descriptor.location.name}))
|
||||
msg = str(resp.status_code)
|
||||
elif descriptor.location.category == 'static_tab':
|
||||
kwargs = {'course_id': course_id,
|
||||
'tab_slug': descriptor.location.name}
|
||||
self._assert_loads('static_tab', kwargs, descriptor)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'course_info':
|
||||
resp = self.client.get(reverse('info', kwargs={'course_id': course_id}))
|
||||
msg = str(resp.status_code)
|
||||
elif descriptor.location.category == 'course_info':
|
||||
self._assert_loads('info', {'course_id': course_id},
|
||||
descriptor)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif descriptor.location.category == 'custom_tag_template':
|
||||
pass
|
||||
else:
|
||||
#print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('jump_to',
|
||||
kwargs={'course_id': course_id,
|
||||
'location': descriptor.location.url()}), follow=True)
|
||||
msg = str(resp.status_code)
|
||||
elif descriptor.location.category == 'custom_tag_template':
|
||||
pass
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg + ": " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif resp.redirect_chain[0][1] != 302:
|
||||
msg = "ERROR on redirect from " + descriptor.location.url()
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
else:
|
||||
|
||||
# check content to make sure there were no rendering failures
|
||||
content = resp.content
|
||||
if content.find("this module is temporarily unavailable") >= 0:
|
||||
msg = "ERROR unavailable module "
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
elif isinstance(descriptor, ErrorDescriptor):
|
||||
msg = "ERROR error descriptor loaded: "
|
||||
msg = msg + descriptor.error_msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
kwargs = {'course_id': course_id,
|
||||
'location': descriptor.location.url()}
|
||||
|
||||
print msg
|
||||
self.assertTrue(all_ok) # fail fast
|
||||
self._assert_loads('jump_to', kwargs, descriptor,
|
||||
expect_redirect=True,
|
||||
check_content=True)
|
||||
|
||||
print "{0}/{1} good".format(num - num_bad, num)
|
||||
log.info("{0}/{1} good".format(num - num_bad, num))
|
||||
self.assertTrue(all_ok)
|
||||
def _assert_loads(self, django_url, kwargs, descriptor,
|
||||
expect_redirect=False,
|
||||
check_content=False):
|
||||
'''
|
||||
Assert that the url loads correctly.
|
||||
If expect_redirect, then also check that we were redirected.
|
||||
If check_content, then check that we don't get
|
||||
an error message about unavailable modules.
|
||||
'''
|
||||
|
||||
url = reverse(django_url, kwargs=kwargs)
|
||||
response = self.client.get(url, follow=True)
|
||||
|
||||
if response.status_code != 200:
|
||||
self.fail('Status %d for page %s' %
|
||||
(response.status_code, descriptor.location.url()))
|
||||
|
||||
if expect_redirect:
|
||||
self.assertEqual(response.redirect_chain[0][1], 302)
|
||||
|
||||
if check_content:
|
||||
unavailable_msg = "this module is temporarily unavailable"
|
||||
self.assertEqual(response.content.find(unavailable_msg), -1)
|
||||
self.assertFalse(isinstance(descriptor, ErrorDescriptor))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
@@ -351,21 +363,13 @@ class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
|
||||
def test_toy_course_loads(self):
|
||||
module_class = 'xmodule.hidden_module.HiddenDescriptor'
|
||||
module_store = XMLModuleStore(TEST_DATA_DIR,
|
||||
default_class='xmodule.hidden_module.HiddenDescriptor',
|
||||
default_class=module_class,
|
||||
course_dirs=['toy'],
|
||||
load_error_modules=True,
|
||||
)
|
||||
load_error_modules=True)
|
||||
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
def test_full_course_loads(self):
|
||||
module_store = XMLModuleStore(TEST_DATA_DIR,
|
||||
default_class='xmodule.hidden_module.HiddenDescriptor',
|
||||
course_dirs=['full'],
|
||||
load_error_modules=True,
|
||||
)
|
||||
self.check_pages_load(module_store)
|
||||
self.check_random_page_loads(module_store)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
@@ -380,12 +384,7 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase):
|
||||
def test_toy_course_loads(self):
|
||||
module_store = modulestore()
|
||||
import_from_xml(module_store, TEST_DATA_DIR, ['toy'])
|
||||
self.check_pages_load(module_store)
|
||||
|
||||
def test_full_course_loads(self):
|
||||
module_store = modulestore()
|
||||
import_from_xml(module_store, TEST_DATA_DIR, ['full'])
|
||||
self.check_pages_load(module_store)
|
||||
self.check_random_page_loads(module_store)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
@@ -415,37 +414,51 @@ class TestNavigation(LoginEnrollmentTestCase):
|
||||
self.enroll(self.full)
|
||||
|
||||
# First request should redirect to ToyVideos
|
||||
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
resp = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
# Don't use no-follow, because state should only be saved once we actually hit the section
|
||||
# Don't use no-follow, because state should
|
||||
# only be saved once we actually hit the section
|
||||
self.assertRedirects(resp, reverse(
|
||||
'courseware_section', kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview',
|
||||
'section': 'Toy_Videos'}))
|
||||
|
||||
# Hitting the couseware tab again should redirect to the first chapter: 'Overview'
|
||||
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
# Hitting the couseware tab again should
|
||||
# redirect to the first chapter: 'Overview'
|
||||
resp = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
|
||||
kwargs={'course_id': self.toy.id, 'chapter': 'Overview'}))
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview'}))
|
||||
|
||||
# Now we directly navigate to a section in a different chapter
|
||||
self.check_for_get_code(200, reverse('courseware_section',
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'secret:magic', 'section': 'toyvideo'}))
|
||||
'chapter': 'secret:magic',
|
||||
'section': 'toyvideo'}))
|
||||
|
||||
# And now hitting the courseware tab should redirect to 'secret:magic'
|
||||
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
resp = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
|
||||
kwargs={'course_id': self.toy.id, 'chapter': 'secret:magic'}))
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'secret:magic'}))
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE)
|
||||
class TestDraftModuleStore(TestCase):
|
||||
def test_get_items_with_course_items(self):
|
||||
store = modulestore()
|
||||
|
||||
# fix was to allow get_items() to take the course_id parameter
|
||||
store.get_items(Location(None, None, 'vertical', None, None), course_id='abc', depth=0)
|
||||
# test success is just getting through the above statement. The bug was that 'course_id' argument was
|
||||
store.get_items(Location(None, None, 'vertical', None, None),
|
||||
course_id='abc', depth=0)
|
||||
|
||||
# test success is just getting through the above statement.
|
||||
# The bug was that 'course_id' argument was
|
||||
# not allowed to be passed in (i.e. was throwing exception)
|
||||
|
||||
|
||||
@@ -472,21 +485,29 @@ class TestViewAuth(LoginEnrollmentTestCase):
|
||||
self.activate_user(self.instructor)
|
||||
|
||||
def test_instructor_pages(self):
|
||||
"""Make sure only instructors for the course or staff can load the instructor
|
||||
"""Make sure only instructors for the course
|
||||
or staff can load the instructor
|
||||
dashboard, the grade views, and student profile pages"""
|
||||
|
||||
# First, try with an enrolled student
|
||||
self.login(self.student, self.password)
|
||||
# shouldn't work before enroll
|
||||
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
self.assertRedirectsNoFollow(response, reverse('about_course', args=[self.toy.id]))
|
||||
response = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
|
||||
self.assertRedirectsNoFollow(response,
|
||||
reverse('about_course',
|
||||
args=[self.toy.id]))
|
||||
self.enroll(self.toy)
|
||||
self.enroll(self.full)
|
||||
# should work now -- redirect to first page
|
||||
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id}))
|
||||
self.assertRedirectsNoFollow(response, reverse('courseware_section', kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview',
|
||||
'section': 'Toy_Videos'}))
|
||||
response = self.client.get(reverse('courseware',
|
||||
kwargs={'course_id': self.toy.id}))
|
||||
self.assertRedirectsNoFollow(response,
|
||||
reverse('courseware_section',
|
||||
kwargs={'course_id': self.toy.id,
|
||||
'chapter': 'Overview',
|
||||
'section': 'Toy_Videos'}))
|
||||
|
||||
def instructor_urls(course):
|
||||
"list of urls that only instructors/staff should be able to see"
|
||||
@@ -494,14 +515,19 @@ class TestViewAuth(LoginEnrollmentTestCase):
|
||||
'instructor_dashboard',
|
||||
'gradebook',
|
||||
'grade_summary',)]
|
||||
urls.append(reverse('student_progress', kwargs={'course_id': course.id,
|
||||
'student_id': get_user(self.student).id}))
|
||||
|
||||
urls.append(reverse('student_progress',
|
||||
kwargs={'course_id': course.id,
|
||||
'student_id': get_user(self.student).id}))
|
||||
return urls
|
||||
|
||||
# shouldn't be able to get to the instructor pages
|
||||
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
# Randomly sample an instructor page
|
||||
url = random.choice(instructor_urls(self.toy) +
|
||||
instructor_urls(self.full))
|
||||
|
||||
# Shouldn't be able to get to the instructor pages
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# Make the instructor staff in the toy course
|
||||
group_name = _course_staff_group_name(self.toy.location)
|
||||
@@ -512,13 +538,13 @@ class TestViewAuth(LoginEnrollmentTestCase):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
# Now should be able to get to the toy course, but not the full course
|
||||
for url in instructor_urls(self.toy):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
url = random.choice(instructor_urls(self.toy))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
for url in instructor_urls(self.full):
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
url = random.choice(instructor_urls(self.full))
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# now also make the instructor staff
|
||||
instructor = get_user(self.instructor)
|
||||
@@ -526,9 +552,10 @@ class TestViewAuth(LoginEnrollmentTestCase):
|
||||
instructor.save()
|
||||
|
||||
# and now should be able to load both
|
||||
for url in instructor_urls(self.toy) + instructor_urls(self.full):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
url = random.choice(instructor_urls(self.toy) +
|
||||
instructor_urls(self.full))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
def run_wrapped(self, test):
|
||||
"""
|
||||
@@ -572,7 +599,8 @@ class TestViewAuth(LoginEnrollmentTestCase):
|
||||
|
||||
def reverse_urls(names, course):
|
||||
"""Reverse a list of course urls"""
|
||||
return [reverse(name, kwargs={'course_id': course.id}) for name in names]
|
||||
return [reverse(name, kwargs={'course_id': course.id})
|
||||
for name in names]
|
||||
|
||||
def dark_student_urls(course):
|
||||
"""
|
||||
@@ -581,7 +609,8 @@ class TestViewAuth(LoginEnrollmentTestCase):
|
||||
"""
|
||||
urls = reverse_urls(['info', 'progress'], course)
|
||||
urls.extend([
|
||||
reverse('book', kwargs={'course_id': course.id, 'book_index': book.title})
|
||||
reverse('book', kwargs={'course_id': course.id,
|
||||
'book_index': book.title})
|
||||
for book in course.textbooks
|
||||
])
|
||||
return urls
|
||||
@@ -600,37 +629,46 @@ class TestViewAuth(LoginEnrollmentTestCase):
|
||||
|
||||
def instructor_urls(course):
|
||||
"""list of urls that only instructors/staff should be able to see"""
|
||||
urls = reverse_urls(['instructor_dashboard', 'gradebook', 'grade_summary'],
|
||||
course)
|
||||
urls = reverse_urls(['instructor_dashboard',
|
||||
'gradebook', 'grade_summary'], course)
|
||||
return urls
|
||||
|
||||
def check_non_staff(course):
|
||||
"""Check that access is right for non-staff in course"""
|
||||
print '=== Checking non-staff access for {0}'.format(course.id)
|
||||
for url in instructor_urls(course) + dark_student_urls(course) + reverse_urls(['courseware'], course):
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
for url in light_student_urls(course):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
# Randomly sample a dark url
|
||||
url = random.choice(instructor_urls(course) +
|
||||
dark_student_urls(course) +
|
||||
reverse_urls(['courseware'], course))
|
||||
print 'checking for 404 on {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
# Randomly sample a light url
|
||||
url = random.choice(light_student_urls(course))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
def check_staff(course):
|
||||
"""Check that access is right for staff in course"""
|
||||
print '=== Checking staff access for {0}'.format(course.id)
|
||||
for url in (instructor_urls(course) +
|
||||
dark_student_urls(course) +
|
||||
light_student_urls(course)):
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
# Randomly sample a url
|
||||
url = random.choice(instructor_urls(course) +
|
||||
dark_student_urls(course) +
|
||||
light_student_urls(course))
|
||||
print 'checking for 200 on {0}'.format(url)
|
||||
self.check_for_get_code(200, url)
|
||||
|
||||
# The student progress tab is not accessible to a student
|
||||
# before launch, so the instructor view-as-student feature should return a 404 as well.
|
||||
# before launch, so the instructor view-as-student feature
|
||||
# should return a 404 as well.
|
||||
# TODO (vshnayder): If this is not the behavior we want, will need
|
||||
# to make access checking smarter and understand both the effective
|
||||
# user (the student), and the requesting user (the prof)
|
||||
url = reverse('student_progress', kwargs={'course_id': course.id,
|
||||
'student_id': get_user(self.student).id})
|
||||
url = reverse('student_progress',
|
||||
kwargs={'course_id': course.id,
|
||||
'student_id': get_user(self.student).id})
|
||||
print 'checking for 404 on view-as-student: {0}'.format(url)
|
||||
self.check_for_get_code(404, url)
|
||||
|
||||
@@ -786,7 +824,7 @@ class TestCourseGrader(LoginEnrollmentTestCase):
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
return grades.grade(self.student_user, fake_request,
|
||||
self.graded_course, model_data_cache)
|
||||
@@ -801,10 +839,12 @@ class TestCourseGrader(LoginEnrollmentTestCase):
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
progress_summary = grades.progress_summary(self.student_user, fake_request,
|
||||
self.graded_course, model_data_cache)
|
||||
progress_summary = grades.progress_summary(self.student_user,
|
||||
fake_request,
|
||||
self.graded_course,
|
||||
model_data_cache)
|
||||
return progress_summary
|
||||
|
||||
def check_grade_percent(self, percent):
|
||||
@@ -820,17 +860,17 @@ class TestCourseGrader(LoginEnrollmentTestCase):
|
||||
input_i4x-edX-graded-problem-H1P3_2_1
|
||||
input_i4x-edX-graded-problem-H1P3_2_2
|
||||
"""
|
||||
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
||||
problem_location = "i4x://edX/graded/problem/%s" % problem_url_name
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_check', })
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_check', })
|
||||
|
||||
resp = self.client.post(modx_url, {
|
||||
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
|
||||
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1],
|
||||
})
|
||||
'input_i4x-edX-graded-problem-%s_2_1' % problem_url_name: responses[0],
|
||||
'input_i4x-edX-graded-problem-%s_2_2' % problem_url_name: responses[1],
|
||||
})
|
||||
print "modx_url", modx_url, "responses", responses
|
||||
print "resp", resp
|
||||
|
||||
@@ -845,9 +885,9 @@ class TestCourseGrader(LoginEnrollmentTestCase):
|
||||
problem_location = self.problem_location(problem_url_name)
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_reset', })
|
||||
kwargs={'course_id': self.graded_course.id,
|
||||
'location': problem_location,
|
||||
'dispatch': 'problem_reset', })
|
||||
|
||||
resp = self.client.post(modx_url)
|
||||
return resp
|
||||
@@ -887,7 +927,8 @@ class TestCourseGrader(LoginEnrollmentTestCase):
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0])
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
# This problem is hidden in an ABTest. Getting it correct doesn't change total grade
|
||||
# This problem is hidden in an ABTest.
|
||||
# Getting it correct doesn't change total grade
|
||||
self.submit_question_answer('H1P3', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.25)
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
@@ -522,6 +522,12 @@ def static_university_profile(request, org_id):
|
||||
"""
|
||||
Return the profile for the particular org_id that does not have any courses.
|
||||
"""
|
||||
# Redirect to the properly capitalized org_id
|
||||
last_path = request.path.split('/')[-1]
|
||||
if last_path != org_id:
|
||||
return redirect('static_university_profile', org_id=org_id)
|
||||
|
||||
# Render template
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
context = dict(courses=[], org_id=org_id)
|
||||
return render_to_response(template_file, context)
|
||||
@@ -533,17 +539,28 @@ def university_profile(request, org_id):
|
||||
"""
|
||||
Return the profile for the particular org_id. 404 if it's not valid.
|
||||
"""
|
||||
virtual_orgs_ids = settings.VIRTUAL_UNIVERSITIES
|
||||
meta_orgs = getattr(settings, 'META_UNIVERSITIES', {})
|
||||
|
||||
# Get all the ids associated with this organization
|
||||
all_courses = modulestore().get_courses()
|
||||
valid_org_ids = set(c.org for c in all_courses).union(settings.VIRTUAL_UNIVERSITIES)
|
||||
if org_id not in valid_org_ids:
|
||||
valid_orgs_ids = set(c.org for c in all_courses)
|
||||
valid_orgs_ids.update(virtual_orgs_ids + meta_orgs.keys())
|
||||
|
||||
if org_id not in valid_orgs_ids:
|
||||
raise Http404("University Profile not found for {0}".format(org_id))
|
||||
|
||||
# Only grab courses for this org...
|
||||
courses = get_courses_by_university(request.user,
|
||||
domain=request.META.get('HTTP_HOST'))[org_id]
|
||||
courses = sort_by_announcement(courses)
|
||||
# Grab all courses for this organization(s)
|
||||
org_ids = set([org_id] + meta_orgs.get(org_id, []))
|
||||
org_courses = []
|
||||
domain = request.META.get('HTTP_HOST')
|
||||
for key in org_ids:
|
||||
cs = get_courses_by_university(request.user, domain=domain)[key]
|
||||
org_courses.extend(cs)
|
||||
|
||||
context = dict(courses=courses, org_id=org_id)
|
||||
org_courses = sort_by_announcement(org_courses)
|
||||
|
||||
context = dict(courses=org_courses, org_id=org_id)
|
||||
template_file = "university_profile/{0}.html".format(org_id).lower()
|
||||
|
||||
return render_to_response(template_file, context)
|
||||
@@ -646,13 +663,13 @@ def submission_history(request, course_id, student_username, location):
|
||||
.format(student_username, location))
|
||||
|
||||
history_entries = StudentModuleHistory.objects \
|
||||
.filter(student_module=student_module).order_by('-created')
|
||||
.filter(student_module=student_module).order_by('-id')
|
||||
|
||||
# If no history records exist, let's force a save to get history started.
|
||||
if not history_entries:
|
||||
student_module.save()
|
||||
history_entries = StudentModuleHistory.objects \
|
||||
.filter(student_module=student_module).order_by('-created')
|
||||
.filter(student_module=student_module).order_by('-id')
|
||||
|
||||
context = {
|
||||
'history_entries': history_entries,
|
||||
|
||||
@@ -76,6 +76,7 @@ LOGGING = get_logger_config(LOG_DIR,
|
||||
COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {})
|
||||
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
|
||||
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', [])
|
||||
META_UNIVERSITIES = ENV_TOKENS.get('META_UNIVERSITIES', {})
|
||||
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
|
||||
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
|
||||
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
|
||||
|
||||
@@ -9,6 +9,7 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
|
||||
SUBDOMAIN_BRANDING['edge'] = 'edge'
|
||||
SUBDOMAIN_BRANDING['preview.edge'] = 'edge'
|
||||
VIRTUAL_UNIVERSITIES = ['edge']
|
||||
META_UNIVERSITIES = {}
|
||||
|
||||
modulestore_options = {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
|
||||
@@ -113,6 +113,9 @@ SUBDOMAIN_BRANDING = {
|
||||
# have an actual course with that org set
|
||||
VIRTUAL_UNIVERSITIES = []
|
||||
|
||||
# Organization that contain other organizations
|
||||
META_UNIVERSITIES = {'UTx': ['UTAustinX']}
|
||||
|
||||
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
|
||||
|
||||
############################## Course static files ##########################
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
23
lms/templates/university_profile/utaustinx.html
Normal file
23
lms/templates/university_profile/utaustinx.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%block name="title"><title>UTAustinX</title></%block>
|
||||
|
||||
<%block name="university_header">
|
||||
<header class="search" style="background: url('/static/images/university/utaustin/utaustin-cover_2025x550.jpg')">
|
||||
<div class="inner-wrapper university-search">
|
||||
<hgroup>
|
||||
<div class="logo">
|
||||
<img src="${static.url('images/university/utaustin/utaustin-standalone_187x80.png')}" />
|
||||
</div>
|
||||
<h1>UTAustinx</h1>
|
||||
</hgroup>
|
||||
</div>
|
||||
</header>
|
||||
</%block>
|
||||
|
||||
<%block name="university_description">
|
||||
<p>The University of Texas at Austin is the top-ranked public university in a nearly 1,000-mile radius, and is ranked in the top 25 universities in the world. Students have been finding their passion in life at UT Austin for more than 130 years, and it has been a member of the prestigious AAU since 1929. UT Austin combines the academic depth and breadth of a world research institute (regularly ranking within the top three producers of doctoral degrees in the country) with the fun and excitement of a big-time collegiate experience. It is currently the fifth-largest university in America, with more than 50,000 students and 3,000 professors across 17 colleges and schools, and is the first major American university to build a medical school in the past 50 years.</p>
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
@@ -1,5 +1,8 @@
|
||||
<%inherit file="base.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.core.urlresolvers import reverse
|
||||
%>
|
||||
|
||||
<%block name="title"><title>UTx</title></%block>
|
||||
|
||||
@@ -19,6 +22,7 @@
|
||||
|
||||
<%block name="university_description">
|
||||
<p>Educating students, providing care for patients, conducting groundbreaking research and serving the needs of Texans and the nation for more than 130 years, The University of Texas System is one of the largest public university systems in the United States, with nine academic universities and six health science centers. Student enrollment exceeded 215,000 in the 2011 academic year. The UT System confers more than one-third of the state’s undergraduate degrees and educates nearly three-fourths of the state’s health care professionals annually. The UT System has an annual operating budget of $13.1 billion (FY 2012) including $2.3 billion in sponsored programs funded by federal, state, local and private sources. With roughly 87,000 employees, the UT System is one of the largest employers in the state.</p>
|
||||
<p>Find out about the <a href="${reverse('university_profile', args=['UTAustinX'])}">University of Texas Austin</a>.</p>
|
||||
</%block>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
38
lms/urls.py
38
lms/urls.py
@@ -69,44 +69,22 @@ urlpatterns = ('',
|
||||
|
||||
url(r'^heartbeat$', include('heartbeat.urls')),
|
||||
|
||||
url(r'^university_profile/UTx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'UTx'}),
|
||||
url(r'^university_profile/WellesleyX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/WellesleyX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'WellesleyX'}),
|
||||
url(r'^university_profile/GeorgetownX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/GeorgetownX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'GeorgetownX'}),
|
||||
|
||||
# Dan accidentally sent out a press release with lower case urls for McGill, Toronto,
|
||||
# Rice, ANU, Delft, and EPFL. Hence the redirects.
|
||||
url(r'^university_profile/McGillX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/McGillX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'McGillX'}),
|
||||
url(r'^university_profile/mcgillx$',
|
||||
RedirectView.as_view(url='/university_profile/McGillX')),
|
||||
|
||||
url(r'^university_profile/TorontoX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/TorontoX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'TorontoX'}),
|
||||
url(r'^university_profile/torontox$',
|
||||
RedirectView.as_view(url='/university_profile/TorontoX')),
|
||||
|
||||
url(r'^university_profile/RiceX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/RiceX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'RiceX'}),
|
||||
url(r'^university_profile/ricex$',
|
||||
RedirectView.as_view(url='/university_profile/RiceX')),
|
||||
|
||||
url(r'^university_profile/ANUx$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/ANUx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'ANUx'}),
|
||||
url(r'^university_profile/anux$',
|
||||
RedirectView.as_view(url='/university_profile/ANUx')),
|
||||
|
||||
url(r'^university_profile/DelftX$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/DelftX$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'DelftX'}),
|
||||
url(r'^university_profile/delftx$',
|
||||
RedirectView.as_view(url='/university_profile/DelftX')),
|
||||
|
||||
url(r'^university_profile/EPFLx$', 'courseware.views.static_university_profile',
|
||||
url(r'^(?i)university_profile/EPFLx$', 'courseware.views.static_university_profile',
|
||||
name="static_university_profile", kwargs={'org_id': 'EPFLx'}),
|
||||
url(r'^university_profile/epflx$',
|
||||
RedirectView.as_view(url='/university_profile/EPFLx')),
|
||||
|
||||
url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile',
|
||||
name="university_profile"),
|
||||
|
||||
Reference in New Issue
Block a user