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
......
'''
Test for lms courseware app
'''
import logging import logging
import json import json
import time import time
import random
from urlparse import urlsplit, urlunsplit from urlparse import urlsplit, urlunsplit
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
...@@ -11,8 +17,6 @@ from django.core.urlresolvers import reverse ...@@ -11,8 +17,6 @@ from django.core.urlresolvers import reverse
from django.test.utils import override_settings from django.test.utils import override_settings
import xmodule.modulestore.django import xmodule.modulestore.django
from xmodule.modulestore.mongo import MongoModuleStore
# Need access to internal func to put users in the right group # Need access to internal func to put users in the right group
from courseware import grades from courseware import grades
...@@ -29,6 +33,7 @@ from xmodule.modulestore.xml import XMLModuleStore ...@@ -29,6 +33,7 @@ from xmodule.modulestore.xml import XMLModuleStore
log = logging.getLogger("mitx." + __name__) log = logging.getLogger("mitx." + __name__)
def parse_json(response): def parse_json(response):
"""Parse response, which is assumed to be json""" """Parse response, which is assumed to be json"""
return json.loads(response.content) return json.loads(response.content)
...@@ -47,7 +52,7 @@ def get_registration(email): ...@@ -47,7 +52,7 @@ def get_registration(email):
def mongo_store_config(data_dir): def mongo_store_config(data_dir):
''' '''
Defines default module store using MongoModuleStore Defines default module store using MongoModuleStore
Use of this config requires mongo to be running Use of this config requires mongo to be running
''' '''
return { return {
...@@ -101,7 +106,10 @@ TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR) ...@@ -101,7 +106,10 @@ TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
class LoginEnrollmentTestCase(TestCase): class LoginEnrollmentTestCase(TestCase):
'''Base TestCase providing support for user creation, activation, login, and course enrollment''' '''
Base TestCase providing support for user creation,
activation, login, and course enrollment
'''
def assertRedirectsNoFollow(self, response, expected_url): def assertRedirectsNoFollow(self, response, expected_url):
""" """
...@@ -112,22 +120,26 @@ class LoginEnrollmentTestCase(TestCase): ...@@ -112,22 +120,26 @@ class LoginEnrollmentTestCase(TestCase):
Some of the code taken from django.test.testcases.py Some of the code taken from django.test.testcases.py
""" """
self.assertEqual(response.status_code, 302, self.assertEqual(response.status_code, 302,
'Response status code was {0} instead of 302'.format(response.status_code)) 'Response status code was %d instead of 302'
% (response.status_code))
url = response['Location'] url = response['Location']
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url) e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
if not (e_scheme or e_netloc): if not (e_scheme or e_netloc):
expected_url = urlunsplit(('http', 'testserver', e_path, e_query, e_fragment)) expected_url = urlunsplit(('http', 'testserver',
e_path, e_query, e_fragment))
self.assertEqual(url, expected_url, "Response redirected to '{0}', expected '{1}'".format( self.assertEqual(url, expected_url,
url, expected_url)) "Response redirected to '%s', expected '%s'" %
(url, expected_url))
def setup_viewtest_user(self): def setup_viewtest_user(self):
'''create a user account, activate, and log in''' '''create a user account, activate, and log in'''
self.viewtest_email = 'view@test.com' self.viewtest_email = 'view@test.com'
self.viewtest_password = 'foo' self.viewtest_password = 'foo'
self.viewtest_username = 'viewtest' self.viewtest_username = 'viewtest'
self.create_account(self.viewtest_username, self.viewtest_email, self.viewtest_password) self.create_account(self.viewtest_username,
self.viewtest_email, self.viewtest_password)
self.activate_user(self.viewtest_email) self.activate_user(self.viewtest_email)
self.login(self.viewtest_email, self.viewtest_password) self.login(self.viewtest_email, self.viewtest_password)
...@@ -185,7 +197,8 @@ class LoginEnrollmentTestCase(TestCase): ...@@ -185,7 +197,8 @@ class LoginEnrollmentTestCase(TestCase):
activation_key = get_registration(email).activation_key activation_key = get_registration(email).activation_key
# and now we try to activate # and now we try to activate
resp = self.client.get(reverse('activate', kwargs={'key': activation_key})) url = reverse('activate', kwargs={'key': activation_key})
resp = self.client.get(url)
return resp return resp
def activate_user(self, email): def activate_user(self, email):
...@@ -205,7 +218,8 @@ class LoginEnrollmentTestCase(TestCase): ...@@ -205,7 +218,8 @@ class LoginEnrollmentTestCase(TestCase):
def try_enroll(self, course): def try_enroll(self, course):
"""Try to enroll. Return bool success instead of asserting it.""" """Try to enroll. Return bool success instead of asserting it."""
data = self._enroll(course) data = self._enroll(course)
print 'Enrollment in {0} result: {1}'.format(course.location.url(), data) print ('Enrollment in %s result: %s'
% (course.location.url(), str(data)))
return data['success'] return data['success']
def enroll(self, course): def enroll(self, course):
...@@ -229,7 +243,8 @@ class LoginEnrollmentTestCase(TestCase): ...@@ -229,7 +243,8 @@ class LoginEnrollmentTestCase(TestCase):
""" """
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(resp.status_code, code, self.assertEqual(resp.status_code, code,
"got code {0} for url '{1}'. Expected code {2}".format(resp.status_code, url, code)) "got code %d for url '%s'. Expected code %d"
% (resp.status_code, url, code))
return resp return resp
def check_for_post_code(self, code, url, data={}): def check_for_post_code(self, code, url, data={}):
...@@ -239,7 +254,8 @@ class LoginEnrollmentTestCase(TestCase): ...@@ -239,7 +254,8 @@ class LoginEnrollmentTestCase(TestCase):
""" """
resp = self.client.post(url, data) resp = self.client.post(url, data)
self.assertEqual(resp.status_code, code, self.assertEqual(resp.status_code, code,
"got code {0} for url '{1}'. Expected code {2}".format(resp.status_code, url, code)) "got code %d for url '%s'. Expected code %d"
% (resp.status_code, url, code))
return resp return resp
...@@ -260,8 +276,10 @@ class ActivateLoginTest(LoginEnrollmentTestCase): ...@@ -260,8 +276,10 @@ class ActivateLoginTest(LoginEnrollmentTestCase):
class PageLoaderTestCase(LoginEnrollmentTestCase): class PageLoaderTestCase(LoginEnrollmentTestCase):
''' Base class that adds a function to load all pages in a modulestore ''' ''' Base class that adds a function to load all pages in a modulestore '''
def check_pages_load(self, module_store): def check_random_page_loads(self, module_store):
"""Make all locations in course load""" '''
Choose a page in the course randomly, and assert that it loads
'''
# enroll in the course before trying to access pages # enroll in the course before trying to access pages
courses = module_store.get_courses() courses = module_store.get_courses()
self.assertEqual(len(courses), 1) self.assertEqual(len(courses), 1)
...@@ -269,77 +287,71 @@ class PageLoaderTestCase(LoginEnrollmentTestCase): ...@@ -269,77 +287,71 @@ class PageLoaderTestCase(LoginEnrollmentTestCase):
self.enroll(course) self.enroll(course)
course_id = course.id course_id = course.id
num = 0 # Search for items in the course
num_bad = 0 # None is treated as a wildcard
all_ok = True course_loc = course.location
location_query = Location(course_loc.tag, course_loc.org,
for descriptor in module_store.get_items( course_loc.course, None, None, None)
Location(None, None, None, None, None)):
items = module_store.get_items(location_query)
num += 1
print "Checking ", descriptor.location.url() if len(items) < 1:
self.fail('Could not retrieve any items from course')
# We have ancillary course information now as modules and we can't simply use 'jump_to' to view them else:
if descriptor.location.category == 'about': descriptor = random.choice(items)
resp = self.client.get(reverse('about_course', kwargs={'course_id': course_id}))
msg = str(resp.status_code) # We have ancillary course information now as modules
# and we can't simply use 'jump_to' to view them
if resp.status_code != 200: if descriptor.location.category == 'about':
msg = "ERROR " + msg self._assert_loads('about_course',
all_ok = False {'course_id': course_id},
num_bad += 1 descriptor)
elif descriptor.location.category == 'static_tab':
resp = self.client.get(reverse('static_tab', kwargs={'course_id': course_id, 'tab_slug': descriptor.location.name})) elif descriptor.location.category == 'static_tab':
msg = str(resp.status_code) kwargs = {'course_id': course_id,
'tab_slug': descriptor.location.name}
if resp.status_code != 200: self._assert_loads('static_tab', kwargs, descriptor)
msg = "ERROR " + msg
all_ok = False elif descriptor.location.category == 'course_info':
num_bad += 1 self._assert_loads('info', {'course_id': course_id},
elif descriptor.location.category == 'course_info': descriptor)
resp = self.client.get(reverse('info', kwargs={'course_id': course_id}))
msg = str(resp.status_code) elif descriptor.location.category == 'custom_tag_template':
pass
if resp.status_code != 200:
msg = "ERROR " + msg else:
all_ok = False
num_bad += 1 kwargs = {'course_id': course_id,
elif descriptor.location.category == 'custom_tag_template': 'location': descriptor.location.url()}
pass
else: self._assert_loads('jump_to', kwargs, descriptor,
#print descriptor.__class__, descriptor.location expect_redirect=True,
resp = self.client.get(reverse('jump_to', check_content=True)
kwargs={'course_id': course_id,
'location': descriptor.location.url()}), follow=True) def _assert_loads(self, django_url, kwargs, descriptor,
msg = str(resp.status_code) expect_redirect=False,
check_content=False):
if resp.status_code != 200: '''
msg = "ERROR " + msg + ": " + descriptor.location.url() Assert that the url loads correctly.
all_ok = False If expect_redirect, then also check that we were redirected.
num_bad += 1 If check_content, then check that we don't get
elif resp.redirect_chain[0][1] != 302: an error message about unavailable modules.
msg = "ERROR on redirect from " + descriptor.location.url() '''
all_ok = False
num_bad += 1 url = reverse(django_url, kwargs=kwargs)
response = self.client.get(url, follow=True)
# check content to make sure there were no rendering failures
content = resp.content if response.status_code != 200:
if content.find("this module is temporarily unavailable") >= 0: self.fail('Status %d for page %s' %
msg = "ERROR unavailable module " (response.status_code, descriptor.location.url()))
all_ok = False
num_bad += 1 if expect_redirect:
elif isinstance(descriptor, ErrorDescriptor): self.assertEqual(response.redirect_chain[0][1], 302)
msg = "ERROR error descriptor loaded: "
msg = msg + descriptor.error_msg if check_content:
all_ok = False unavailable_msg = "this module is temporarily unavailable"
num_bad += 1 self.assertEqual(response.content.find(unavailable_msg), -1)
self.assertFalse(isinstance(descriptor, ErrorDescriptor))
print msg
self.assertTrue(all_ok) # fail fast
print "{0}/{1} good".format(num - num_bad, num)
log.info("{0}/{1} good".format(num - num_bad, num))
self.assertTrue(all_ok)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
...@@ -351,21 +363,13 @@ class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase): ...@@ -351,21 +363,13 @@ class TestCoursesLoadTestCase_XmlModulestore(PageLoaderTestCase):
xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django._MODULESTORES = {}
def test_toy_course_loads(self): def test_toy_course_loads(self):
module_class = 'xmodule.hidden_module.HiddenDescriptor'
module_store = XMLModuleStore(TEST_DATA_DIR, module_store = XMLModuleStore(TEST_DATA_DIR,
default_class='xmodule.hidden_module.HiddenDescriptor', default_class=module_class,
course_dirs=['toy'], course_dirs=['toy'],
load_error_modules=True, load_error_modules=True)
)
self.check_pages_load(module_store)
def test_full_course_loads(self): self.check_random_page_loads(module_store)
module_store = XMLModuleStore(TEST_DATA_DIR,
default_class='xmodule.hidden_module.HiddenDescriptor',
course_dirs=['full'],
load_error_modules=True,
)
self.check_pages_load(module_store)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
...@@ -380,12 +384,7 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase): ...@@ -380,12 +384,7 @@ class TestCoursesLoadTestCase_MongoModulestore(PageLoaderTestCase):
def test_toy_course_loads(self): def test_toy_course_loads(self):
module_store = modulestore() module_store = modulestore()
import_from_xml(module_store, TEST_DATA_DIR, ['toy']) import_from_xml(module_store, TEST_DATA_DIR, ['toy'])
self.check_pages_load(module_store) self.check_random_page_loads(module_store)
def test_full_course_loads(self):
module_store = modulestore()
import_from_xml(module_store, TEST_DATA_DIR, ['full'])
self.check_pages_load(module_store)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
...@@ -415,37 +414,51 @@ class TestNavigation(LoginEnrollmentTestCase): ...@@ -415,37 +414,51 @@ class TestNavigation(LoginEnrollmentTestCase):
self.enroll(self.full) self.enroll(self.full)
# First request should redirect to ToyVideos # First request should redirect to ToyVideos
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id})) resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.toy.id}))
# Don't use no-follow, because state should only be saved once we actually hit the section # Don't use no-follow, because state should
# only be saved once we actually hit the section
self.assertRedirects(resp, reverse( self.assertRedirects(resp, reverse(
'courseware_section', kwargs={'course_id': self.toy.id, 'courseware_section', kwargs={'course_id': self.toy.id,
'chapter': 'Overview', 'chapter': 'Overview',
'section': 'Toy_Videos'})) 'section': 'Toy_Videos'}))
# Hitting the couseware tab again should redirect to the first chapter: 'Overview' # Hitting the couseware tab again should
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id})) # redirect to the first chapter: 'Overview'
resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.toy.id}))
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
kwargs={'course_id': self.toy.id, 'chapter': 'Overview'})) kwargs={'course_id': self.toy.id,
'chapter': 'Overview'}))
# Now we directly navigate to a section in a different chapter # Now we directly navigate to a section in a different chapter
self.check_for_get_code(200, reverse('courseware_section', self.check_for_get_code(200, reverse('courseware_section',
kwargs={'course_id': self.toy.id, kwargs={'course_id': self.toy.id,
'chapter': 'secret:magic', 'section': 'toyvideo'})) 'chapter': 'secret:magic',
'section': 'toyvideo'}))
# And now hitting the courseware tab should redirect to 'secret:magic' # And now hitting the courseware tab should redirect to 'secret:magic'
resp = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id})) resp = self.client.get(reverse('courseware',
kwargs={'course_id': self.toy.id}))
self.assertRedirectsNoFollow(resp, reverse('courseware_chapter', self.assertRedirectsNoFollow(resp, reverse('courseware_chapter',
kwargs={'course_id': self.toy.id, 'chapter': 'secret:magic'})) kwargs={'course_id': self.toy.id,
'chapter': 'secret:magic'}))
@override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_DRAFT_MONGO_MODULESTORE)
class TestDraftModuleStore(TestCase): class TestDraftModuleStore(TestCase):
def test_get_items_with_course_items(self): def test_get_items_with_course_items(self):
store = modulestore() store = modulestore()
# fix was to allow get_items() to take the course_id parameter # fix was to allow get_items() to take the course_id parameter
store.get_items(Location(None, None, 'vertical', None, None), course_id='abc', depth=0) store.get_items(Location(None, None, 'vertical', None, None),
# test success is just getting through the above statement. The bug was that 'course_id' argument was course_id='abc', depth=0)
# test success is just getting through the above statement.
# The bug was that 'course_id' argument was
# not allowed to be passed in (i.e. was throwing exception) # not allowed to be passed in (i.e. was throwing exception)
...@@ -472,21 +485,29 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -472,21 +485,29 @@ class TestViewAuth(LoginEnrollmentTestCase):
self.activate_user(self.instructor) self.activate_user(self.instructor)
def test_instructor_pages(self): def test_instructor_pages(self):
"""Make sure only instructors for the course or staff can load the instructor """Make sure only instructors for the course
or staff can load the instructor
dashboard, the grade views, and student profile pages""" dashboard, the grade views, and student profile pages"""
# First, try with an enrolled student # First, try with an enrolled student
self.login(self.student, self.password) self.login(self.student, self.password)
# shouldn't work before enroll # shouldn't work before enroll
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id})) response = self.client.get(reverse('courseware',
self.assertRedirectsNoFollow(response, reverse('about_course', args=[self.toy.id])) kwargs={'course_id': self.toy.id}))
self.assertRedirectsNoFollow(response,
reverse('about_course',
args=[self.toy.id]))
self.enroll(self.toy) self.enroll(self.toy)
self.enroll(self.full) self.enroll(self.full)
# should work now -- redirect to first page # should work now -- redirect to first page
response = self.client.get(reverse('courseware', kwargs={'course_id': self.toy.id})) response = self.client.get(reverse('courseware',
self.assertRedirectsNoFollow(response, reverse('courseware_section', kwargs={'course_id': self.toy.id, kwargs={'course_id': self.toy.id}))
'chapter': 'Overview', self.assertRedirectsNoFollow(response,
'section': 'Toy_Videos'})) reverse('courseware_section',
kwargs={'course_id': self.toy.id,
'chapter': 'Overview',
'section': 'Toy_Videos'}))
def instructor_urls(course): def instructor_urls(course):
"list of urls that only instructors/staff should be able to see" "list of urls that only instructors/staff should be able to see"
...@@ -494,14 +515,19 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -494,14 +515,19 @@ class TestViewAuth(LoginEnrollmentTestCase):
'instructor_dashboard', 'instructor_dashboard',
'gradebook', 'gradebook',
'grade_summary',)] 'grade_summary',)]
urls.append(reverse('student_progress', kwargs={'course_id': course.id,
'student_id': get_user(self.student).id})) urls.append(reverse('student_progress',
kwargs={'course_id': course.id,
'student_id': get_user(self.student).id}))
return urls return urls
# shouldn't be able to get to the instructor pages # Randomly sample an instructor page
for url in instructor_urls(self.toy) + instructor_urls(self.full): url = random.choice(instructor_urls(self.toy) +
print 'checking for 404 on {0}'.format(url) instructor_urls(self.full))
self.check_for_get_code(404, url)
# Shouldn't be able to get to the instructor pages
print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url)
# Make the instructor staff in the toy course # Make the instructor staff in the toy course
group_name = _course_staff_group_name(self.toy.location) group_name = _course_staff_group_name(self.toy.location)
...@@ -512,13 +538,13 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -512,13 +538,13 @@ class TestViewAuth(LoginEnrollmentTestCase):
self.login(self.instructor, self.password) self.login(self.instructor, self.password)
# Now should be able to get to the toy course, but not the full course # Now should be able to get to the toy course, but not the full course
for url in instructor_urls(self.toy): url = random.choice(instructor_urls(self.toy))
print 'checking for 200 on {0}'.format(url) print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url) self.check_for_get_code(200, url)
for url in instructor_urls(self.full): url = random.choice(instructor_urls(self.full))
print 'checking for 404 on {0}'.format(url) print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url) self.check_for_get_code(404, url)
# now also make the instructor staff # now also make the instructor staff
instructor = get_user(self.instructor) instructor = get_user(self.instructor)
...@@ -526,9 +552,10 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -526,9 +552,10 @@ class TestViewAuth(LoginEnrollmentTestCase):
instructor.save() instructor.save()
# and now should be able to load both # and now should be able to load both
for url in instructor_urls(self.toy) + instructor_urls(self.full): url = random.choice(instructor_urls(self.toy) +
print 'checking for 200 on {0}'.format(url) instructor_urls(self.full))
self.check_for_get_code(200, url) print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
def run_wrapped(self, test): def run_wrapped(self, test):
""" """
...@@ -572,7 +599,8 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -572,7 +599,8 @@ class TestViewAuth(LoginEnrollmentTestCase):
def reverse_urls(names, course): def reverse_urls(names, course):
"""Reverse a list of course urls""" """Reverse a list of course urls"""
return [reverse(name, kwargs={'course_id': course.id}) for name in names] return [reverse(name, kwargs={'course_id': course.id})
for name in names]
def dark_student_urls(course): def dark_student_urls(course):
""" """
...@@ -581,7 +609,8 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -581,7 +609,8 @@ class TestViewAuth(LoginEnrollmentTestCase):
""" """
urls = reverse_urls(['info', 'progress'], course) urls = reverse_urls(['info', 'progress'], course)
urls.extend([ urls.extend([
reverse('book', kwargs={'course_id': course.id, 'book_index': book.title}) reverse('book', kwargs={'course_id': course.id,
'book_index': book.title})
for book in course.textbooks for book in course.textbooks
]) ])
return urls return urls
...@@ -600,37 +629,46 @@ class TestViewAuth(LoginEnrollmentTestCase): ...@@ -600,37 +629,46 @@ class TestViewAuth(LoginEnrollmentTestCase):
def instructor_urls(course): def instructor_urls(course):
"""list of urls that only instructors/staff should be able to see""" """list of urls that only instructors/staff should be able to see"""
urls = reverse_urls(['instructor_dashboard', 'gradebook', 'grade_summary'], urls = reverse_urls(['instructor_dashboard',
course) 'gradebook', 'grade_summary'], course)
return urls return urls
def check_non_staff(course): def check_non_staff(course):
"""Check that access is right for non-staff in course""" """Check that access is right for non-staff in course"""
print '=== Checking non-staff access for {0}'.format(course.id) print '=== Checking non-staff access for {0}'.format(course.id)
for url in instructor_urls(course) + dark_student_urls(course) + reverse_urls(['courseware'], course):
print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url)
for url in light_student_urls(course): # Randomly sample a dark url
print 'checking for 200 on {0}'.format(url) url = random.choice(instructor_urls(course) +
self.check_for_get_code(200, url) dark_student_urls(course) +
reverse_urls(['courseware'], course))
print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url)
# Randomly sample a light url
url = random.choice(light_student_urls(course))
print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
def check_staff(course): def check_staff(course):
"""Check that access is right for staff in course""" """Check that access is right for staff in course"""
print '=== Checking staff access for {0}'.format(course.id) print '=== Checking staff access for {0}'.format(course.id)
for url in (instructor_urls(course) +
dark_student_urls(course) + # Randomly sample a url
light_student_urls(course)): url = random.choice(instructor_urls(course) +
print 'checking for 200 on {0}'.format(url) dark_student_urls(course) +
self.check_for_get_code(200, url) light_student_urls(course))
print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
# The student progress tab is not accessible to a student # The student progress tab is not accessible to a student
# before launch, so the instructor view-as-student feature should return a 404 as well. # before launch, so the instructor view-as-student feature
# should return a 404 as well.
# TODO (vshnayder): If this is not the behavior we want, will need # TODO (vshnayder): If this is not the behavior we want, will need
# to make access checking smarter and understand both the effective # to make access checking smarter and understand both the effective
# user (the student), and the requesting user (the prof) # user (the student), and the requesting user (the prof)
url = reverse('student_progress', kwargs={'course_id': course.id, url = reverse('student_progress',
'student_id': get_user(self.student).id}) kwargs={'course_id': course.id,
'student_id': get_user(self.student).id})
print 'checking for 404 on view-as-student: {0}'.format(url) print 'checking for 404 on view-as-student: {0}'.format(url)
self.check_for_get_code(404, url) self.check_for_get_code(404, url)
...@@ -786,7 +824,7 @@ class TestCourseGrader(LoginEnrollmentTestCase): ...@@ -786,7 +824,7 @@ class TestCourseGrader(LoginEnrollmentTestCase):
self.graded_course.id, self.student_user, self.graded_course) self.graded_course.id, self.student_user, self.graded_course)
fake_request = self.factory.get(reverse('progress', fake_request = self.factory.get(reverse('progress',
kwargs={'course_id': self.graded_course.id})) kwargs={'course_id': self.graded_course.id}))
return grades.grade(self.student_user, fake_request, return grades.grade(self.student_user, fake_request,
self.graded_course, model_data_cache) self.graded_course, model_data_cache)
...@@ -801,10 +839,12 @@ class TestCourseGrader(LoginEnrollmentTestCase): ...@@ -801,10 +839,12 @@ class TestCourseGrader(LoginEnrollmentTestCase):
self.graded_course.id, self.student_user, self.graded_course) self.graded_course.id, self.student_user, self.graded_course)
fake_request = self.factory.get(reverse('progress', fake_request = self.factory.get(reverse('progress',
kwargs={'course_id': self.graded_course.id})) kwargs={'course_id': self.graded_course.id}))
progress_summary = grades.progress_summary(self.student_user, fake_request, progress_summary = grades.progress_summary(self.student_user,
self.graded_course, model_data_cache) fake_request,
self.graded_course,
model_data_cache)
return progress_summary return progress_summary
def check_grade_percent(self, percent): def check_grade_percent(self, percent):
...@@ -820,17 +860,17 @@ class TestCourseGrader(LoginEnrollmentTestCase): ...@@ -820,17 +860,17 @@ class TestCourseGrader(LoginEnrollmentTestCase):
input_i4x-edX-graded-problem-H1P3_2_1 input_i4x-edX-graded-problem-H1P3_2_1
input_i4x-edX-graded-problem-H1P3_2_2 input_i4x-edX-graded-problem-H1P3_2_2
""" """
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name) problem_location = "i4x://edX/graded/problem/%s" % problem_url_name
modx_url = reverse('modx_dispatch', modx_url = reverse('modx_dispatch',
kwargs={'course_id': self.graded_course.id, kwargs={'course_id': self.graded_course.id,
'location': problem_location, 'location': problem_location,
'dispatch': 'problem_check', }) 'dispatch': 'problem_check', })
resp = self.client.post(modx_url, { resp = self.client.post(modx_url, {
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0], 'input_i4x-edX-graded-problem-%s_2_1' % problem_url_name: responses[0],
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1], 'input_i4x-edX-graded-problem-%s_2_2' % problem_url_name: responses[1],
}) })
print "modx_url", modx_url, "responses", responses print "modx_url", modx_url, "responses", responses
print "resp", resp print "resp", resp
...@@ -845,9 +885,9 @@ class TestCourseGrader(LoginEnrollmentTestCase): ...@@ -845,9 +885,9 @@ class TestCourseGrader(LoginEnrollmentTestCase):
problem_location = self.problem_location(problem_url_name) problem_location = self.problem_location(problem_url_name)
modx_url = reverse('modx_dispatch', modx_url = reverse('modx_dispatch',
kwargs={'course_id': self.graded_course.id, kwargs={'course_id': self.graded_course.id,
'location': problem_location, 'location': problem_location,
'dispatch': 'problem_reset', }) 'dispatch': 'problem_reset', })
resp = self.client.post(modx_url) resp = self.client.post(modx_url)
return resp return resp
...@@ -887,7 +927,8 @@ class TestCourseGrader(LoginEnrollmentTestCase): ...@@ -887,7 +927,8 @@ class TestCourseGrader(LoginEnrollmentTestCase):
self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0]) self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0])
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
# This problem is hidden in an ABTest. Getting it correct doesn't change total grade # This problem is hidden in an ABTest.
# Getting it correct doesn't change total grade
self.submit_question_answer('H1P3', ['Correct', 'Correct']) self.submit_question_answer('H1P3', ['Correct', 'Correct'])
self.check_grade_percent(0.25) self.check_grade_percent(0.25)
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0]) self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
......
...@@ -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