Commit 8ace7fe5 by cahrens

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
parents 16f4a3f9 a50f1ca6
...@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore ...@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_edit_unit_full(self): def test_edit_unit_full(self):
self.check_edit_unit('full') 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): def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check that there's actually content in the 'question' field # check that there's actually content in the 'question' field
self.assertGreater(len(items[0].question),0) self.assertGreater(len(items[0].question),0)
def test_xlint_fails(self):
err_cnt = perform_xlint('common/test/data', ['full'])
self.assertGreater(err_cnt, 0)
def test_delete(self): def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
......
'''
Utilities for contentstore tests
'''
#pylint: disable=W0603
import json import json
import copy import copy
from uuid import uuid4 from uuid import uuid4
...@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase): ...@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase):
collection with templates before running the TestCase collection with templates before running the TestCase
and drops it they are finished. """ and drops it they are finished. """
def _pre_setup(self): @staticmethod
super(ModuleStoreTestCase, self)._pre_setup() 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 # Use a uuid to differentiate
# the mongo collections on jenkins. # the mongo collections on jenkins.
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE) cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE test_modulestore = cls.orig_modulestore
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex 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()"
xmodule.modulestore.django._MODULESTORES = {} 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): def _post_teardown(self):
# Make sure you flush out the modulestore. '''
# Drop the collection at the end of the test, Flush everything we created except the templates
# otherwise there will be lingering collections leftover '''
# from executing the tests. # Flush anything that is not a template
xmodule.modulestore.django._MODULESTORES = {} ModuleStoreTestCase.flush_mongo_except_templates()
xmodule.modulestore.django.modulestore().collection.drop()
settings.MODULESTORE = self.orig_MODULESTORE
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown() super(ModuleStoreTestCase, self)._post_teardown()
......
import logging
from django.conf import settings from django.conf import settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import copy
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] 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): def get_modulestore(location):
""" """
...@@ -137,7 +141,7 @@ def compute_unit_state(unit): ...@@ -137,7 +141,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS 'private' content is editabled and not visible in the LMS
""" """
if unit.cms.is_draft: if getattr(unit, 'is_draft', False):
try: try:
modulestore('direct').get_item(unit.location) modulestore('direct').get_item(unit.location)
return UnitState.draft return UnitState.draft
...@@ -187,3 +191,35 @@ class CoursePageNames: ...@@ -187,3 +191,35 @@ class CoursePageNames:
SettingsGrading = "settings_grading" SettingsGrading = "settings_grading"
CourseOutline = "course_index" CourseOutline = "course_index"
Checklists = "checklists" 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 ...@@ -41,7 +41,7 @@ from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule 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 functools import partial
from xmodule.contentstore.django import contentstore 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 ...@@ -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 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 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, \ 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 xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \ from contentstore.course_info_model import get_course_updates, \
...@@ -73,7 +74,8 @@ log = logging.getLogger(__name__) ...@@ -73,7 +74,8 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] 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_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
...@@ -188,7 +190,7 @@ def course_index(request, org, course, name): ...@@ -188,7 +190,7 @@ def course_index(request, org, course, name):
'coursename': name 'coursename': name
}) })
course = modulestore().get_item(location) course = modulestore().get_item(location, depth=3)
sections = course.get_children() sections = course.get_children()
return render_to_response('overview.html', { return render_to_response('overview.html', {
...@@ -208,19 +210,14 @@ def course_index(request, org, course, name): ...@@ -208,19 +210,14 @@ def course_index(request, org, course, name):
@login_required @login_required
def edit_subsection(request, location): def edit_subsection(request, location):
# check that we have permissions to edit this item # 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() 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) lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
preview_link = get_lms_link_for_item(location, preview=True) 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 # make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential': if item.location.category != 'sequential':
...@@ -253,12 +250,6 @@ def edit_subsection(request, location): ...@@ -253,12 +250,6 @@ def edit_subsection(request, location):
can_view_live = True can_view_live = True
break 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', return render_to_response('edit_subsection.html',
{'subsection': item, {'subsection': item,
'context_course': course, 'context_course': course,
...@@ -283,19 +274,13 @@ def edit_unit(request, location): ...@@ -283,19 +274,13 @@ def edit_unit(request, location):
id: A Location URL id: A Location URL
""" """
# check that we have permissions to edit this item course = get_course_for_item(location)
if not has_access(request.user, location): if not has_access(request.user, course.location):
raise PermissionDenied() 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) component_templates = defaultdict(list)
...@@ -454,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None): ...@@ -454,9 +439,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, request.POST) ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
...@@ -1279,15 +1271,48 @@ def course_advanced_updates(request, org, course, name): ...@@ -1279,15 +1271,48 @@ def course_advanced_updates(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request) real_method = get_request_method(request)
if real_method == 'GET': if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE': 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': 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 # 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 @ensure_csrf_cookie
@login_required @login_required
......
...@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor ...@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.core import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
import copy
class CourseMetadata(object): class CourseMetadata(object):
''' '''
...@@ -39,7 +39,7 @@ class CourseMetadata(object): ...@@ -39,7 +39,7 @@ class CourseMetadata(object):
return course return course
@classmethod @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. Decode the json into CourseMetadata and save any changed attrs to the db.
...@@ -48,10 +48,16 @@ class CourseMetadata(object): ...@@ -48,10 +48,16 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False 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(): for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload? # 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 continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v: if hasattr(descriptor, k) and getattr(descriptor, k) != v:
......
...@@ -142,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -142,4 +142,4 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # 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 = { ...@@ -58,6 +58,10 @@ MODULESTORE = {
'direct': { 'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': modulestore_options
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
} }
} }
......
...@@ -40,7 +40,6 @@ class CmsNamespace(Namespace): ...@@ -40,7 +40,6 @@ class CmsNamespace(Namespace):
""" """
Namespace with fields common to all blocks in Studio 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_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) 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) empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
...@@ -118,7 +118,7 @@ class LoncapaProblem(object): ...@@ -118,7 +118,7 @@ class LoncapaProblem(object):
# 3. Assign from the OS's random number generator # 3. Assign from the OS's random number generator
self.seed = state.get('seed', seed) self.seed = state.get('seed', seed)
if self.seed is None: 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', {}) self.student_answers = state.get('student_answers', {})
if 'correct_map' in state: if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map']) self.correct_map.set_dict(state['correct_map'])
......
...@@ -80,16 +80,17 @@ class CorrectMap(object): ...@@ -80,16 +80,17 @@ class CorrectMap(object):
Special migration case: Special migration case:
If correct_map is a one-level dict, then convert it to the new dict of dicts format. 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
# empty current dict self.__init__()
self.__init__()
# create new dict entries # create new dict entries
if correct_map and not isinstance(correct_map.values()[0], dict):
# special migration
for k in correct_map: for k in correct_map:
self.set(k, correct_map[k]) self.set(k, correctness=correct_map[k])
else: else:
self.__init__()
for k in correct_map: for k in correct_map:
self.set(k, **correct_map[k]) self.set(k, **correct_map[k])
......
...@@ -17,6 +17,7 @@ import logging ...@@ -17,6 +17,7 @@ import logging
import numbers import numbers
import numpy import numpy
import os import os
import sys
import random import random
import re import re
import requests import requests
...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception): ...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception):
class ResponseError(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 pass
class StudentInputError(Exception): class StudentInputError(Exception):
'''
Error for an invalid student input.
For example, submitting a string when the problem expects a number
'''
pass pass
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse): ...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse):
import sys import sys
type, value, traceback = sys.exc_info() 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 cgi.escape(student_answer)), traceback
if correct: if correct:
...@@ -1072,13 +1078,10 @@ def sympy_check2(): ...@@ -1072,13 +1078,10 @@ def sympy_check2():
correct = self.context['correct'] correct = self.context['correct']
messages = self.context['messages'] messages = self.context['messages']
overall_message = self.context['overall_message'] overall_message = self.context['overall_message']
except Exception as err: except Exception as err:
print "oops in customresponse (code) error %s" % err self._handle_exec_exception(err)
print "context = ", self.context
print traceback.format_exc()
# Notify student
raise StudentInputError(
"Error: Problem could not be evaluated with your input")
else: else:
# self.code is not a string; assume its a function # self.code is not a string; assume its a function
...@@ -1105,13 +1108,9 @@ def sympy_check2(): ...@@ -1105,13 +1108,9 @@ def sympy_check2():
nargs, args, kwargs)) nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs) ret = fn(*args[:nargs], **kwargs)
except Exception as err: except Exception as err:
log.error("oops in customresponse (cfn) error %s" % err) self._handle_exec_exception(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)
if type(ret) == dict: if type(ret) == dict:
...@@ -1147,9 +1146,9 @@ def sympy_check2(): ...@@ -1147,9 +1146,9 @@ def sympy_check2():
correct = [] correct = []
messages = [] messages = []
for input_dict in input_list: for input_dict in input_list:
correct.append('correct' correct.append('correct'
if input_dict['ok'] else 'incorrect') 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) if 'msg' in input_dict else None)
messages.append(msg) messages.append(msg)
...@@ -1157,7 +1156,7 @@ def sympy_check2(): ...@@ -1157,7 +1156,7 @@ def sympy_check2():
# Raise an exception # Raise an exception
else: else:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise Exception( raise ResponseError(
"CustomResponse: check function returned an invalid dict") "CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value, # The check function can return a boolean value,
...@@ -1174,7 +1173,7 @@ def sympy_check2(): ...@@ -1174,7 +1173,7 @@ def sympy_check2():
correct_map.set_overall_message(overall_message) correct_map.set_overall_message(overall_message)
for k in range(len(idset)): for k in range(len(idset)):
npoints = (self.maxpoints[idset[k]] npoints = (self.maxpoints[idset[k]]
if correct[k] == 'correct' else 0) if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k], correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints) npoints=npoints)
...@@ -1227,6 +1226,22 @@ def sympy_check2(): ...@@ -1227,6 +1226,22 @@ def sympy_check2():
return {self.answer_ids[0]: self.expect} return {self.answer_ids[0]: self.expect}
return self.default_answer_map 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): ...@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse):
submission = [json.loads(student_answers[ submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)] k]) for k in sorted(self.answer_ids)]
self.context.update({'submission': submission}) 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 = CorrectMap()
cmap.set_dict(dict(zip(sorted( cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct']))) self.answer_ids), self.context['correct'])))
...@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse):
option_scoring = dict([(option['id'], { option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']), 'correctness': choices.get(option['choice']),
'points': scoring.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 scoring_map[inputfield.get('id')] = option_scoring
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <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': if status == 'correct':
correctness = 'correct' correctness = 'correct'
...@@ -30,9 +30,9 @@ ...@@ -30,9 +30,9 @@
class="choicegroup_${correctness}" class="choicegroup_${correctness}"
% endif % endif
% endif % endif
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <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" checked="true"
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
......
...@@ -13,6 +13,8 @@ import textwrap ...@@ -13,6 +13,8 @@ import textwrap
from . import test_system from . import test_system
import capa.capa_problem as lcp import capa.capa_problem as lcp
from capa.responsetypes import LoncapaProblemError, \
StudentInputError, ResponseError
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat from capa.xqueue_interface import dateformat
...@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest): ...@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest):
# Message is interpreted as an "overall message" # Message is interpreted as an "overall message"
self.assertEqual(correct_map.get_overall_message(), 'Message text') 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 # Construct a script that will raise an exception
script = textwrap.dedent(""" script = textwrap.dedent("""
...@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest): ...@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(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'}) problem.grade_answers({'1_2_1': '42'})
def test_invalid_dict_exception(self): def test_invalid_dict_exception(self):
...@@ -889,7 +901,7 @@ class CustomResponseTest(ResponseTest): ...@@ -889,7 +901,7 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
...@@ -922,6 +934,18 @@ class SchematicResponseTest(ResponseTest): ...@@ -922,6 +934,18 @@ class SchematicResponseTest(ResponseTest):
# is what we expect) # is what we expect)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') 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): class AnnotationResponseTest(ResponseTest):
from response_xml_factory import AnnotationResponseXMLFactory from response_xml_factory import AnnotationResponseXMLFactory
......
...@@ -14,7 +14,7 @@ from capa.util import convert_files_to_filenames ...@@ -14,7 +14,7 @@ from capa.util import convert_files_to_filenames
from .progress import Progress from .progress import Progress
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor 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 xblock.core import Integer, Scope, String, Boolean, Object, Float
from .fields import Timedelta, Date from .fields import Timedelta, Date
from xmodule.util.date_utils import time_to_datetime from xmodule.util.date_utils import time_to_datetime
...@@ -91,7 +91,7 @@ class CapaFields(object): ...@@ -91,7 +91,7 @@ class CapaFields(object):
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) 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={}) 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) 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) 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) display_name = String(help="Display name for this module", scope=Scope.settings)
...@@ -449,7 +449,14 @@ class CapaModule(CapaFields, XModule): ...@@ -449,7 +449,14 @@ class CapaModule(CapaFields, XModule):
return 'Error' return 'Error'
before = self.get_progress() 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() after = self.get_progress()
d.update({ d.update({
'progress_changed': after != before, 'progress_changed': after != before,
...@@ -571,7 +578,7 @@ class CapaModule(CapaFields, XModule): ...@@ -571,7 +578,7 @@ class CapaModule(CapaFields, XModule):
# save any state changes that may occur # save any state changes that may occur
self.set_state_from_lcp() self.set_state_from_lcp()
return response return response
def get_answer(self, get): def get_answer(self, get):
''' '''
...@@ -720,9 +727,24 @@ class CapaModule(CapaFields, XModule): ...@@ -720,9 +727,24 @@ class CapaModule(CapaFields, XModule):
try: try:
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
self.set_state_from_lcp() self.set_state_from_lcp()
except StudentInputError as inst:
log.exception("StudentInputError in capa_module:problem_check") except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
return {'success': inst.message} 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: except Exception, err:
if self.system.DEBUG: if self.system.DEBUG:
msg = "Error checking problem: " + str(err) msg = "Error checking problem: " + str(err)
...@@ -773,7 +795,7 @@ class CapaModule(CapaFields, XModule): ...@@ -773,7 +795,7 @@ class CapaModule(CapaFields, XModule):
event_info['answers'] = answers event_info['answers'] = answers
# Too late. Cannot submit # 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' event_info['failure'] = 'closed'
self.system.track_function('save_problem_fail', event_info) self.system.track_function('save_problem_fail', event_info)
return {'success': False, return {'success': False,
...@@ -793,7 +815,7 @@ class CapaModule(CapaFields, XModule): ...@@ -793,7 +815,7 @@ class CapaModule(CapaFields, XModule):
self.system.track_function('save_problem_success', event_info) self.system.track_function('save_problem_success', event_info)
msg = "Your answers have been saved" 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." msg += " but not graded. Hit 'Check' to grade them."
return {'success': True, return {'success': True,
'msg': msg} 'msg': msg}
......
...@@ -9,11 +9,12 @@ from xblock.core import Integer, Scope, String, Boolean, List ...@@ -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 xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date from .fields import Date
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", 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", V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"] "student_attempts", "ready_to_reset"]
...@@ -66,9 +67,9 @@ class CombinedOpenEndedFields(object): ...@@ -66,9 +67,9 @@ class CombinedOpenEndedFields(object):
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings) 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, graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
scope=Scope.settings) 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) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) 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): class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
...@@ -118,7 +119,7 @@ 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: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -190,8 +191,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -190,8 +191,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def get_score(self): def get_score(self):
return self.child_module.get_score() return self.child_module.get_score()
#def max_score(self): def max_score(self):
# return self.child_module.max_score() return self.child_module.max_score()
def get_progress(self): def get_progress(self):
return self.child_module.get_progress() return self.child_module.get_progress()
......
class InvalidDefinitionError(Exception): class InvalidDefinitionError(Exception):
pass pass
class NotFoundError(Exception): class NotFoundError(Exception):
pass 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 ...@@ -10,6 +10,7 @@ from collections import namedtuple
from .exceptions import InvalidLocationError, InsufficientSpecificationError from .exceptions import InvalidLocationError, InsufficientSpecificationError
from xmodule.errortracker import ErrorLog, make_error_tracker from xmodule.errortracker import ErrorLog, make_error_tracker
from bson.son import SON
log = logging.getLogger('mitx.' + 'modulestore') log = logging.getLogger('mitx.' + 'modulestore')
...@@ -457,3 +458,13 @@ class ModuleStoreBase(ModuleStore): ...@@ -457,3 +458,13 @@ class ModuleStoreBase(ModuleStore):
if c.id == course_id: if c.id == course_id:
return c return c
return None 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
from datetime import datetime from datetime import datetime
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location, namedtuple_to_son
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
import logging
DRAFT = 'draft' DRAFT = 'draft'
...@@ -15,11 +16,11 @@ def as_draft(location): ...@@ -15,11 +16,11 @@ def as_draft(location):
def wrap_draft(item): 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 draft, and `False` otherwise. Sets the item's location to the
non-draft location in either case 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) item.location = item.location._replace(revision=None)
return item return item
...@@ -55,11 +56,10 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -55,11 +56,10 @@ class DraftModuleStore(ModuleStoreBase):
get_children() to cache. None indicates to cache all descendents 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: 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: 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): def get_instance(self, course_id, location, depth=0):
""" """
...@@ -67,11 +67,10 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -67,11 +67,10 @@ class DraftModuleStore(ModuleStoreBase):
TODO (vshnayder): this may want to live outside the modulestore eventually 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: 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: 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): def get_items(self, location, course_id=None, depth=0):
""" """
...@@ -88,9 +87,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -88,9 +87,8 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) 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=depth)
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=depth)
items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=0)
draft_locs_found = set(item.location._replace(revision=None) for item in draft_items) draft_locs_found = set(item.location._replace(revision=None) for item in draft_items)
non_draft_items = [ non_draft_items = [
...@@ -118,7 +116,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -118,7 +116,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(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) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_item(draft_loc, data) return super(DraftModuleStore, self).update_item(draft_loc, data)
...@@ -133,7 +131,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -133,7 +131,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(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) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_children(draft_loc, children) return super(DraftModuleStore, self).update_children(draft_loc, children)
...@@ -149,7 +147,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -149,7 +147,7 @@ class DraftModuleStore(ModuleStoreBase):
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(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) self.clone_item(location, draft_loc)
if 'is_draft' in metadata: if 'is_draft' in metadata:
...@@ -192,3 +190,36 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -192,3 +190,36 @@ class DraftModuleStore(ModuleStoreBase):
""" """
super(DraftModuleStore, self).clone_item(location, as_draft(location)) super(DraftModuleStore, self).clone_item(location, as_draft(location))
super(DraftModuleStore, self).delete_item(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 ...@@ -3,7 +3,6 @@ import sys
import logging import logging
import copy import copy
from bson.son import SON
from collections import namedtuple from collections import namedtuple
from fs.osfs import OSFS from fs.osfs import OSFS
from itertools import repeat from itertools import repeat
...@@ -19,7 +18,7 @@ from xmodule.error_module import ErrorDescriptor ...@@ -19,7 +18,7 @@ from xmodule.error_module import ErrorDescriptor
from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError
from xblock.core import Scope from xblock.core import Scope
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location, namedtuple_to_son
from .draft import DraftModuleStore from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
DuplicateItemError) DuplicateItemError)
...@@ -202,16 +201,6 @@ def location_to_query(location, wildcard=True): ...@@ -202,16 +201,6 @@ def location_to_query(location, wildcard=True):
return query 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') metadata_cache_key = attrgetter('org', 'course')
...@@ -383,6 +372,13 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -383,6 +372,13 @@ class MongoModuleStore(ModuleStoreBase):
item['location'] = item['_id'] item['location'] = item['_id']
del 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): def _cache_children(self, items, depth=0):
""" """
Returns a dictionary mapping Location -> item data, populated with json data Returns a dictionary mapping Location -> item data, populated with json data
...@@ -407,13 +403,10 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -407,13 +403,10 @@ class MongoModuleStore(ModuleStoreBase):
# Load all children by id. See # Load all children by id. See
# http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or # http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or
# for or-query syntax # for or-query syntax
to_process = []
if children: if children:
query = { to_process = self._query_children_for_cache_children(children)
'_id': {'$in': [namedtuple_to_son(Location(child)) for child in children]}
}
to_process = self.collection.find(query)
else:
to_process = []
# If depth is None, then we just recurse until we hit all the descendents # If depth is None, then we just recurse until we hit all the descendents
if depth is not None: if depth is not None:
depth -= 1 depth -= 1
......
...@@ -136,3 +136,4 @@ def delete_course(modulestore, contentstore, source_location, commit = False): ...@@ -136,3 +136,4 @@ def delete_course(modulestore, contentstore, source_location, commit = False):
modulestore.delete_item(source_location) modulestore.delete_item(source_location)
return True return True
...@@ -356,6 +356,26 @@ def remap_namespace(module, target_location_namespace): ...@@ -356,6 +356,26 @@ def remap_namespace(module, target_location_namespace):
return module 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): def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category):
err_cnt = 0 err_cnt = 0
...@@ -440,6 +460,13 @@ def perform_xlint(data_dir, course_dirs, ...@@ -440,6 +460,13 @@ def perform_xlint(data_dir, course_dirs,
err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential") err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential")
# constrain that sequentials only have 'verticals' # constrain that sequentials only have 'verticals'
err_cnt += validate_category_hierarchy(module_store, course_id, "sequential", "vertical") 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 # check for a presence of a course marketing video
location_elements = course_id.split('/') location_elements = course_id.split('/')
...@@ -456,3 +483,5 @@ def perform_xlint(data_dir, course_dirs, ...@@ -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" 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: else:
print "This course can be imported successfully." print "This course can be imported successfully."
return err_cnt
...@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware") ...@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware")
# attempts specified in xml definition overrides this. # attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1 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 #The highest score allowed for the overall xmodule and for each rubric point
MAX_SCORE_ALLOWED = 50 MAX_SCORE_ALLOWED = 50
...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module(): ...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module():
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -153,13 +149,9 @@ class CombinedOpenEndedV1Module(): ...@@ -153,13 +149,9 @@ class CombinedOpenEndedV1Module():
raise raise
self.display_due_date = self.timeinfo.display_due_date 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) self.rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_string = stringify_children(definition['rubric']) 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 #Static data is passed to the child modules to render
self.static_data = { self.static_data = {
......
...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object): ...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object):
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
return {'success': success, 'html': html, 'rubric_scores': rubric_scores} 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) rubric_dict = self.render_rubric(rubric_string)
success = rubric_dict['success'] success = rubric_dict['success']
rubric_feedback = rubric_dict['html'] rubric_feedback = rubric_dict['html']
...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object): ...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object):
log.error(error_message) log.error(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
if int(total) != int(max_score): return int(total)
#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)
def extract_categories(self, element): def extract_categories(self, element):
''' '''
......
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 ...@@ -11,7 +11,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope 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 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 ...@@ -27,13 +27,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object): 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) use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, scope=Scope.settings) default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.",default=IS_GRADED, 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) 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) 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) max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),scope=Scope.student_state) 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): class PeerGradingModule(PeerGradingFields, XModule):
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
metadata: metadata:
display_name: Open Ended Response display_name: Open Ended Response
max_attempts: 1 max_attempts: 1
max_score: 1
is_graded: False is_graded: False
version: 1 version: 1
display_name: Open Ended Response display_name: Open Ended Response
skip_spelling_checks: False skip_spelling_checks: False
accept_file_upload: False accept_file_upload: False
weight: ""
data: | data: |
<combinedopenended> <combinedopenended>
<rubric> <rubric>
......
...@@ -6,6 +6,7 @@ metadata: ...@@ -6,6 +6,7 @@ metadata:
link_to_location: None link_to_location: None
is_graded: False is_graded: False
max_grade: 1 max_grade: 1
weight: ""
data: | data: |
<peergrading> <peergrading>
</peergrading> </peergrading>
......
...@@ -7,6 +7,8 @@ import random ...@@ -7,6 +7,8 @@ import random
import xmodule import xmodule
import capa import capa
from capa.responsetypes import StudentInputError, \
LoncapaProblemError, ResponseError
from xmodule.capa_module import CapaModule from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
...@@ -407,7 +409,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -407,7 +409,7 @@ class CapaModuleTest(unittest.TestCase):
mock_html.return_value = "Test HTML" mock_html.return_value = "Test HTML"
# Check the problem # 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) result = module.check_problem(get_request_dict)
# Expect that the problem is marked correct # Expect that the problem is marked correct
...@@ -428,7 +430,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -428,7 +430,7 @@ class CapaModuleTest(unittest.TestCase):
mock_is_correct.return_value = False mock_is_correct.return_value = False
# Check the problem # Check the problem
get_request_dict = { CapaFactory.input_key(): '0'} get_request_dict = {CapaFactory.input_key(): '0'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
# Expect that the problem is marked correct # Expect that the problem is marked correct
...@@ -446,7 +448,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -446,7 +448,7 @@ class CapaModuleTest(unittest.TestCase):
with patch('xmodule.capa_module.CapaModule.closed') as mock_closed: with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
mock_closed.return_value = True mock_closed.return_value = True
with self.assertRaises(xmodule.exceptions.NotFoundError): 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) module.check_problem(get_request_dict)
# Expect that number of attempts NOT incremented # Expect that number of attempts NOT incremented
...@@ -492,7 +494,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -492,7 +494,7 @@ class CapaModuleTest(unittest.TestCase):
mock_is_queued.return_value = True mock_is_queued.return_value = True
mock_get_queuetime.return_value = datetime.datetime.now() 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) result = module.check_problem(get_request_dict)
# Expect an AJAX alert message in 'success' # Expect an AJAX alert message in 'success'
...@@ -502,21 +504,61 @@ class CapaModuleTest(unittest.TestCase): ...@@ -502,21 +504,61 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(module.attempts, 1) self.assertEqual(module.attempts, 1)
def test_check_problem_student_input_error(self): def test_check_problem_error(self):
module = CapaFactory.create(attempts=1)
# Simulate a student input exception # Try each exception that capa_module should handle
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: for exception_class in [StudentInputError,
mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') LoncapaProblemError,
ResponseError]:
get_request_dict = { CapaFactory.input_key(): '3.14'} # Create the module
result = module.check_problem(get_request_dict) 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' # Expect an AJAX alert message in 'success'
self.assertTrue('test error' in result['success']) self.assertTrue('test error' in result['success'])
# Expect that the number of attempts is NOT incremented # We DO include traceback information for staff users
self.assertEqual(module.attempts, 1) 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): def test_reset_problem(self):
...@@ -573,11 +615,11 @@ class CapaModuleTest(unittest.TestCase): ...@@ -573,11 +615,11 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(done=False) module = CapaFactory.create(done=False)
# Save the problem # 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) result = module.save_problem(get_request_dict)
# Expect that answers are saved to the problem # 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) self.assertEqual(module.lcp.student_answers, expected_answers)
# Expect that the result is success # Expect that the result is success
...@@ -592,7 +634,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -592,7 +634,7 @@ class CapaModuleTest(unittest.TestCase):
mock_closed.return_value = True mock_closed.return_value = True
# Try to save the problem # 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) result = module.save_problem(get_request_dict)
# Expect that the result is failure # Expect that the result is failure
...@@ -603,7 +645,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -603,7 +645,7 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize='always', done=True) module = CapaFactory.create(rerandomize='always', done=True)
# Try to save # 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) result = module.save_problem(get_request_dict)
# Expect that we cannot save # Expect that we cannot save
...@@ -614,7 +656,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -614,7 +656,7 @@ class CapaModuleTest(unittest.TestCase):
module = CapaFactory.create(rerandomize='never', done=True) module = CapaFactory.create(rerandomize='never', done=True)
# Try to save # 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) result = module.save_problem(get_request_dict)
# Expect that we succeed # Expect that we succeed
...@@ -626,7 +668,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -626,7 +668,7 @@ class CapaModuleTest(unittest.TestCase):
# Just in case, we also check what happens if we have # Just in case, we also check what happens if we have
# more attempts than allowed. # more attempts than allowed.
attempts = random.randint(1, 10) 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") self.assertEqual(module.check_button_name(), "Final Check")
module = CapaFactory.create(attempts=attempts, max_attempts=attempts) module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
...@@ -636,14 +678,14 @@ class CapaModuleTest(unittest.TestCase): ...@@ -636,14 +678,14 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(module.check_button_name(), "Final Check") self.assertEqual(module.check_button_name(), "Final Check")
# Otherwise, button name is "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") 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") self.assertEqual(module.check_button_name(), "Check")
# If no limit on attempts, then always show "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") self.assertEqual(module.check_button_name(), "Check")
module = CapaFactory.create(attempts=0) module = CapaFactory.create(attempts=0)
......
...@@ -5,11 +5,15 @@ import unittest ...@@ -5,11 +5,15 @@ import unittest
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild 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.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module 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 xmodule.modulestore import Location
from lxml import etree from lxml import etree
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
from datetime import datetime from datetime import datetime
import logging
log = logging.getLogger(__name__)
from . import test_system from . import test_system
...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location, 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): def test_latest_answer_empty(self):
...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase):
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message") self.mock_xqueue.send_to_queue.return_value = (None, "Message")
def constructed_callback(dispatch="score_update"): def constructed_callback(dispatch="score_update"):
return dispatch 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} 'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase):
class CombinedOpenEndedModuleTest(unittest.TestCase): class CombinedOpenEndedModuleTest(unittest.TestCase):
location = Location(["i4x", "edX", "open_ended", "combinedopenended", location = Location(["i4x", "edX", "open_ended", "combinedopenended",
"SampleQuestion"]) "SampleQuestion"])
definition_template = """
<combinedopenended attempts="10000">
{rubric}
{prompt}
<task>
{task1}
</task>
<task>
{task2}
</task>
</combinedopenended>
"""
prompt = "<prompt>This is a question prompt</prompt>" prompt = "<prompt>This is a question prompt</prompt>"
rubric = '''<rubric><rubric> rubric = '''<rubric><rubric>
<category> <category>
...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
</openendedparam> </openendedparam>
</openended>''' </openended>'''
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]} 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): def setUp(self):
self.test_system = test_system()
# TODO: this constructor call is definitely wrong, but neither branch # TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this. # of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system, self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = self.combinedoe.update_task_states() changed = self.combinedoe.update_task_states()
self.assertTrue(changed) 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 ...@@ -43,13 +43,15 @@ rake pep8 > pep8.log || cat pep8.log
rake pylint > pylint.log || cat pylint.log rake pylint > pylint.log || cat pylint.log
TESTS_FAILED=0 TESTS_FAILED=0
# Run the python unit tests
rake test_cms[false] || TESTS_FAILED=1 rake test_cms[false] || TESTS_FAILED=1
rake test_lms[false] || TESTS_FAILED=1 rake test_lms[false] || TESTS_FAILED=1
rake test_common/lib/capa || TESTS_FAILED=1 rake test_common/lib/capa || TESTS_FAILED=1
rake test_common/lib/xmodule || TESTS_FAILED=1 rake test_common/lib/xmodule || TESTS_FAILED=1
# Don't run the lms jasmine tests for now because
# they mostly all fail anyhow # Run the jaavascript unit tests
# rake phantomjs_jasmine_lms || true rake phantomjs_jasmine_lms || TESTS_FAILED=1
rake phantomjs_jasmine_cms || TESTS_FAILED=1 rake phantomjs_jasmine_cms || TESTS_FAILED=1
rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1 rake phantomjs_jasmine_common/lib/xmodule || TESTS_FAILED=1
......
...@@ -3,10 +3,11 @@ import unittest ...@@ -3,10 +3,11 @@ import unittest
import threading import threading
import json import json
import urllib import urllib
import urlparse
import time import time
from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler from mock_xqueue_server import MockXQueueServer, MockXQueueRequestHandler
from nose.plugins.skip import SkipTest
class MockXQueueServerTest(unittest.TestCase): class MockXQueueServerTest(unittest.TestCase):
''' '''
...@@ -22,11 +23,16 @@ class MockXQueueServerTest(unittest.TestCase): ...@@ -22,11 +23,16 @@ class MockXQueueServerTest(unittest.TestCase):
def setUp(self): 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 # Create the server
server_port = 8034 server_port = 8034
self.server_url = 'http://127.0.0.1:%d' % server_port self.server_url = 'http://127.0.0.1:%d' % server_port
self.server = MockXQueueServer(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 # Start the server in a separate daemon thread
server_thread = threading.Thread(target=self.server.serve_forever) server_thread = threading.Thread(target=self.server.serve_forever)
...@@ -48,18 +54,18 @@ class MockXQueueServerTest(unittest.TestCase): ...@@ -48,18 +54,18 @@ class MockXQueueServerTest(unittest.TestCase):
callback_url = 'http://127.0.0.1:8000/test_callback' callback_url = 'http://127.0.0.1:8000/test_callback'
grade_header = json.dumps({'lms_callback_url': callback_url, grade_header = json.dumps({'lms_callback_url': callback_url,
'lms_key': 'test_queuekey', 'lms_key': 'test_queuekey',
'queue_name': 'test_queue'}) 'queue_name': 'test_queue'})
grade_body = json.dumps({'student_info': 'test', grade_body = json.dumps({'student_info': 'test',
'grader_payload': 'test', 'grader_payload': 'test',
'student_response': 'test'}) 'student_response': 'test'})
grade_request = {'xqueue_header': grade_header, grade_request = {'xqueue_header': grade_header,
'xqueue_body': grade_body} 'xqueue_body': grade_body}
response_handle = urllib.urlopen(self.server_url + '/xqueue/submit', response_handle = urllib.urlopen(self.server_url + '/xqueue/submit',
urllib.urlencode(grade_request)) urllib.urlencode(grade_request))
response_dict = json.loads(response_handle.read()) response_dict = json.loads(response_handle.read())
...@@ -71,8 +77,8 @@ class MockXQueueServerTest(unittest.TestCase): ...@@ -71,8 +77,8 @@ class MockXQueueServerTest(unittest.TestCase):
# Expect that the server tries to post back the grading info # Expect that the server tries to post back the grading info
xqueue_body = json.dumps({'correct': True, 'score': 1, xqueue_body = json.dumps({'correct': True, 'score': 1,
'msg': '<div></div>'}) 'msg': '<div></div>'})
expected_callback_dict = {'xqueue_header': grade_header, expected_callback_dict = {'xqueue_header': grade_header,
'xqueue_body': xqueue_body} 'xqueue_body': xqueue_body}
MockXQueueRequestHandler.post_to_url.assert_called_with(callback_url, 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 ...@@ -11,7 +11,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 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 django.views.decorators.csrf import csrf_exempt
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
...@@ -23,7 +23,7 @@ from .models import StudentModule ...@@ -23,7 +23,7 @@ from .models import StudentModule
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from student.models import unique_id_for_user from student.models import unique_id_for_user
from xmodule.errortracker import exc_info_to_str 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 import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
...@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -443,9 +443,19 @@ def modx_dispatch(request, dispatch, location, course_id):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, p) ajax_return = instance.handle_ajax(dispatch, p)
# If we can't find the module, respond with a 404
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 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: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
......
...@@ -522,6 +522,12 @@ def static_university_profile(request, org_id): ...@@ -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. 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() template_file = "university_profile/{0}.html".format(org_id).lower()
context = dict(courses=[], org_id=org_id) context = dict(courses=[], org_id=org_id)
return render_to_response(template_file, context) return render_to_response(template_file, context)
...@@ -533,17 +539,28 @@ def university_profile(request, org_id): ...@@ -533,17 +539,28 @@ def university_profile(request, org_id):
""" """
Return the profile for the particular org_id. 404 if it's not valid. 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() all_courses = modulestore().get_courses()
valid_org_ids = set(c.org for c in all_courses).union(settings.VIRTUAL_UNIVERSITIES) valid_orgs_ids = set(c.org for c in all_courses)
if org_id not in valid_org_ids: 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)) raise Http404("University Profile not found for {0}".format(org_id))
# Only grab courses for this org... # Grab all courses for this organization(s)
courses = get_courses_by_university(request.user, org_ids = set([org_id] + meta_orgs.get(org_id, []))
domain=request.META.get('HTTP_HOST'))[org_id] org_courses = []
courses = sort_by_announcement(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)
org_courses = sort_by_announcement(org_courses)
context = dict(courses=courses, org_id=org_id) context = dict(courses=org_courses, org_id=org_id)
template_file = "university_profile/{0}.html".format(org_id).lower() template_file = "university_profile/{0}.html".format(org_id).lower()
return render_to_response(template_file, context) return render_to_response(template_file, context)
...@@ -646,13 +663,13 @@ def submission_history(request, course_id, student_username, location): ...@@ -646,13 +663,13 @@ def submission_history(request, course_id, student_username, location):
.format(student_username, location)) .format(student_username, location))
history_entries = StudentModuleHistory.objects \ 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 no history records exist, let's force a save to get history started.
if not history_entries: if not history_entries:
student_module.save() student_module.save()
history_entries = StudentModuleHistory.objects \ history_entries = StudentModuleHistory.objects \
.filter(student_module=student_module).order_by('-created') .filter(student_module=student_module).order_by('-id')
context = { context = {
'history_entries': history_entries, 'history_entries': history_entries,
......
...@@ -76,6 +76,7 @@ LOGGING = get_logger_config(LOG_DIR, ...@@ -76,6 +76,7 @@ LOGGING = get_logger_config(LOG_DIR,
COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {}) COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {})
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {}) SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
VIRTUAL_UNIVERSITIES = ENV_TOKENS.get('VIRTUAL_UNIVERSITIES', []) 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_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL", '')
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '') COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY", '')
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull') CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
......
...@@ -9,6 +9,7 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False ...@@ -9,6 +9,7 @@ MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = False
SUBDOMAIN_BRANDING['edge'] = 'edge' SUBDOMAIN_BRANDING['edge'] = 'edge'
SUBDOMAIN_BRANDING['preview.edge'] = 'edge' SUBDOMAIN_BRANDING['preview.edge'] = 'edge'
VIRTUAL_UNIVERSITIES = ['edge'] VIRTUAL_UNIVERSITIES = ['edge']
META_UNIVERSITIES = {}
modulestore_options = { modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor', 'default_class': 'xmodule.raw_module.RawDescriptor',
......
...@@ -113,6 +113,9 @@ SUBDOMAIN_BRANDING = { ...@@ -113,6 +113,9 @@ SUBDOMAIN_BRANDING = {
# have an actual course with that org set # have an actual course with that org set
VIRTUAL_UNIVERSITIES = [] VIRTUAL_UNIVERSITIES = []
# Organization that contain other organizations
META_UNIVERSITIES = {'UTx': ['UTAustinX']}
COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE" COMMENTS_SERVICE_KEY = "PUT_YOUR_API_KEY_HERE"
############################## Course static files ########################## ############################## Course static files ##########################
......
<%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()}
<%inherit file="base.html" /> <%inherit file="base.html" />
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%!
from django.core.urlresolvers import reverse
%>
<%block name="title"><title>UTx</title></%block> <%block name="title"><title>UTx</title></%block>
...@@ -19,6 +22,7 @@ ...@@ -19,6 +22,7 @@
<%block name="university_description"> <%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>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> </%block>
${parent.body()} ${parent.body()}
...@@ -69,44 +69,22 @@ urlpatterns = ('', ...@@ -69,44 +69,22 @@ urlpatterns = ('',
url(r'^heartbeat$', include('heartbeat.urls')), url(r'^heartbeat$', include('heartbeat.urls')),
url(r'^university_profile/UTx$', 'courseware.views.static_university_profile', url(r'^(?i)university_profile/WellesleyX$', 'courseware.views.static_university_profile',
name="static_university_profile", kwargs={'org_id': 'UTx'}),
url(r'^university_profile/WellesleyX$', 'courseware.views.static_university_profile',
name="static_university_profile", kwargs={'org_id': 'WellesleyX'}), 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'}), name="static_university_profile", kwargs={'org_id': 'GeorgetownX'}),
url(r'^(?i)university_profile/McGillX$', 'courseware.views.static_university_profile',
# 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',
name="static_university_profile", kwargs={'org_id': 'McGillX'}), name="static_university_profile", kwargs={'org_id': 'McGillX'}),
url(r'^university_profile/mcgillx$', url(r'^(?i)university_profile/TorontoX$', 'courseware.views.static_university_profile',
RedirectView.as_view(url='/university_profile/McGillX')),
url(r'^university_profile/TorontoX$', 'courseware.views.static_university_profile',
name="static_university_profile", kwargs={'org_id': 'TorontoX'}), name="static_university_profile", kwargs={'org_id': 'TorontoX'}),
url(r'^university_profile/torontox$', url(r'^(?i)university_profile/RiceX$', 'courseware.views.static_university_profile',
RedirectView.as_view(url='/university_profile/TorontoX')),
url(r'^university_profile/RiceX$', 'courseware.views.static_university_profile',
name="static_university_profile", kwargs={'org_id': 'RiceX'}), name="static_university_profile", kwargs={'org_id': 'RiceX'}),
url(r'^university_profile/ricex$', url(r'^(?i)university_profile/ANUx$', 'courseware.views.static_university_profile',
RedirectView.as_view(url='/university_profile/RiceX')),
url(r'^university_profile/ANUx$', 'courseware.views.static_university_profile',
name="static_university_profile", kwargs={'org_id': 'ANUx'}), name="static_university_profile", kwargs={'org_id': 'ANUx'}),
url(r'^university_profile/anux$', url(r'^(?i)university_profile/DelftX$', 'courseware.views.static_university_profile',
RedirectView.as_view(url='/university_profile/ANUx')),
url(r'^university_profile/DelftX$', 'courseware.views.static_university_profile',
name="static_university_profile", kwargs={'org_id': 'DelftX'}), name="static_university_profile", kwargs={'org_id': 'DelftX'}),
url(r'^university_profile/delftx$', url(r'^(?i)university_profile/EPFLx$', 'courseware.views.static_university_profile',
RedirectView.as_view(url='/university_profile/DelftX')),
url(r'^university_profile/EPFLx$', 'courseware.views.static_university_profile',
name="static_university_profile", kwargs={'org_id': 'EPFLx'}), 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', url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile',
name="university_profile"), name="university_profile"),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment