diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index cac4757218..bbaf3f3a6b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,6 +5,13 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
+LMS: Problem rescoring. Added options on the Grades tab of the
+Instructor Dashboard to allow all students' submissions for a
+particular problem to be rescored. Also supports resetting all
+students' number of attempts to zero. Provides a list of background
+tasks that are currently running for the course, and an option to
+see a history of background tasks for a given problem.
+
LMS: Forums. Added handling for case where discussion module can get `None` as
value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
diff --git a/cms/djangoapps/auth/authz.py b/cms/djangoapps/auth/authz.py
index 71b5e97bc2..58b63abd23 100644
--- a/cms/djangoapps/auth/authz.py
+++ b/cms/djangoapps/auth/authz.py
@@ -39,8 +39,6 @@ def get_users_in_course_group_by_role(location, role):
'''
Create all permission groups for a new course and subscribe the caller into those roles
'''
-
-
def create_all_course_groups(creator, location):
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
create_new_course_group(creator, location, STAFF_ROLE_NAME)
@@ -57,13 +55,11 @@ def create_new_course_group(creator, location, role):
return
-'''
-This is to be called only by either a command line code path or through a app which has already
-asserted permissions
-'''
-
-
def _delete_course_group(location):
+ '''
+ This is to be called only by either a command line code path or through a app which has already
+ asserted permissions
+ '''
# remove all memberships
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
for user in instructors.user_set.all():
@@ -75,13 +71,11 @@ def _delete_course_group(location):
user.groups.remove(staff)
user.save()
-'''
-This is to be called only by either a command line code path or through an app which has already
-asserted permissions to do this action
-'''
-
-
def _copy_course_group(source, dest):
+ '''
+ This is to be called only by either a command line code path or through an app which has already
+ asserted permissions to do this action
+ '''
instructors = Group.objects.get(name=get_course_groupname_for_role(source, INSTRUCTOR_ROLE_NAME))
new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
for user in instructors.user_set.all():
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index 92ad5a9bb5..0b7cb11d2a 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -1,5 +1,5 @@
-#pylint: disable=C0111
-#pylint: disable=W0621
+# pylint: disable=C0111
+# pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true
@@ -20,7 +20,7 @@ COURSE_ORG = 'MITx'
########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$')
-def i_visit_the_studio_homepage(step):
+def i_visit_the_studio_homepage(_step):
# To make this go to port 8001, put
# LETTUCE_SERVER_PORT = 8001
# in your settings.py file.
@@ -30,17 +30,17 @@ def i_visit_the_studio_homepage(step):
@step('I am logged into Studio$')
-def i_am_logged_into_studio(step):
+def i_am_logged_into_studio(_step):
log_into_studio()
@step('I confirm the alert$')
-def i_confirm_with_ok(step):
+def i_confirm_with_ok(_step):
world.browser.get_alert().accept()
@step(u'I press the "([^"]*)" delete icon$')
-def i_press_the_category_delete_icon(step, category):
+def i_press_the_category_delete_icon(_step, category):
if category == 'section':
css = 'a.delete-button.delete-section-button span.delete-icon'
elif category == 'subsection':
@@ -51,7 +51,7 @@ def i_press_the_category_delete_icon(step, category):
@step('I have opened a new course in Studio$')
-def i_have_opened_a_new_course(step):
+def i_have_opened_a_new_course(_step):
open_new_course()
@@ -78,7 +78,6 @@ def create_studio_user(
registration.register(studio_user)
registration.activate()
-
def fill_in_course_info(
name=COURSE_NAME,
org=COURSE_ORG,
@@ -149,6 +148,7 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(date_css, desired_date)
# hit TAB to get to the time field
e = world.css_find(date_css).first
+ # pylint: disable=W0212
e._element.send_keys(Keys.TAB)
world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first
diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py
index 9d63fa73c8..989c73e010 100644
--- a/cms/djangoapps/contentstore/features/section.py
+++ b/cms/djangoapps/contentstore/features/section.py
@@ -1,5 +1,5 @@
-#pylint: disable=C0111
-#pylint: disable=W0621
+# pylint: disable=C0111
+# pylint: disable=W0621
from lettuce import world, step
from common import *
@@ -8,7 +8,7 @@ from nose.tools import assert_equal
############### ACTIONS ####################
-@step('I click the new section link$')
+@step('I click the New Section link$')
def i_click_new_section_link(_step):
link_css = 'a.new-courseware-section-button'
world.css_click(link_css)
diff --git a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
index 3aca2aee92..1fbd965871 100644
--- a/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
+++ b/cms/djangoapps/contentstore/features/studio-overview-togglesection.py
@@ -1,5 +1,5 @@
-#pylint: disable=C0111
-#pylint: disable=W0621
+# pylint: disable=C0111
+# pylint: disable=W0621
from lettuce import world, step
from common import *
diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py
index fd8624999e..c48b36a5aa 100644
--- a/cms/djangoapps/contentstore/features/video.py
+++ b/cms/djangoapps/contentstore/features/video.py
@@ -6,13 +6,13 @@ from lettuce import world, step
@step('when I view the video it does not have autoplay enabled')
-def does_not_autoplay(step):
+def does_not_autoplay(_step):
assert world.css_find('.video')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play')
@step('creating a video takes a single click')
-def video_takes_a_single_click(step):
+def video_takes_a_single_click(_step):
assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']")
assert(world.is_css_present('.xmodule_VideoModule'))
diff --git a/cms/djangoapps/contentstore/module_info_model.py b/cms/djangoapps/contentstore/module_info_model.py
index f7d1bbd8fe..e361c97875 100644
--- a/cms/djangoapps/contentstore/module_info_model.py
+++ b/cms/djangoapps/contentstore/module_info_model.py
@@ -39,10 +39,7 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
def set_module_info(store, location, post_data):
module = None
try:
- if location.revision is None:
- module = store.get_item(location)
- else:
- module = store.get_item(location)
+ module = store.get_item(location)
except:
pass
diff --git a/cms/djangoapps/contentstore/tests/test_checklists.py b/cms/djangoapps/contentstore/tests/test_checklists.py
index 54bc726092..0e5cd9b884 100644
--- a/cms/djangoapps/contentstore/tests/test_checklists.py
+++ b/cms/djangoapps/contentstore/tests/test_checklists.py
@@ -99,6 +99,7 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name,
'checklist_index': 2})
+
def get_first_item(checklist):
return checklist['items'][0]
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 9346d2189d..d24deacecf 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -132,7 +132,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
- location = descriptor.location._replace(name='.' + descriptor.location.name)
+ location = descriptor.location.replace(name='.' + descriptor.location.name)
resp = self.client.get(reverse('edit_unit', kwargs={'location': location.url()}))
self.assertEqual(resp.status_code, 400)
@@ -224,7 +224,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft_store.clone_item(html_module.location, html_module.location)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None])
- new_graceperiod = timedelta(**{'hours': 1})
+ new_graceperiod = timedelta(hours=1)
self.assertNotIn('graceperiod', own_metadata(html_module))
html_module.lms.graceperiod = new_graceperiod
@@ -369,7 +369,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'''
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
-
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.data, '6 hours')
@@ -617,12 +616,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0)
- def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
+ def verify_content_existence(self, store, root_dir, location, dirname, category_name, filename_suffix=''):
filesystem = OSFS(root_dir / 'test_export')
self.assertTrue(filesystem.exists(dirname))
query_loc = Location('i4x', location.org, location.course, category_name, None)
- items = modulestore.get_items(query_loc)
+ items = store.get_items(query_loc)
for item in items:
filesystem = OSFS(root_dir / ('test_export/' + dirname))
@@ -768,7 +767,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_prefetch_children(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
-
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
@@ -864,7 +862,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_create_course_duplicate_course(self):
"""Test new course creation - error path"""
- resp = self.client.post(reverse('create_new_course'), self.course_data)
+ self.client.post(reverse('create_new_course'), self.course_data)
resp = self.client.post(reverse('create_new_course'), self.course_data)
data = parse_json(resp)
self.assertEqual(resp.status_code, 200)
@@ -872,7 +870,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_create_course_duplicate_number(self):
"""Test new course creation - error path"""
- resp = self.client.post(reverse('create_new_course'), self.course_data)
+ self.client.post(reverse('create_new_course'), self.course_data)
self.course_data['display_name'] = 'Robot Super Course Two'
resp = self.client.post(reverse('create_new_course'), self.course_data)
@@ -1090,11 +1088,9 @@ class ContentStoreTest(ModuleStoreTestCase):
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
-
def test_import_metadata_with_attempts_empty_string(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['simple'])
-
did_load_item = False
try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index 6f766ff7f5..0bfa70e4f5 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -224,14 +224,14 @@ def add_extra_panel_tab(tab_type, course):
@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
+ # Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
- #Check to see if open ended panel is defined in the course
+ # Check to see if open ended panel is defined in the course
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs:
- #Add panel to the tabs if it is not defined
+ # Add panel to the tabs if it is not defined
course_tabs.append(tab_panel)
changed = True
return changed, course_tabs
@@ -244,14 +244,14 @@ def remove_extra_panel_tab(tab_type, course):
@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
+ # Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
- #Check to see if open ended panel is defined in the course
+ # Check to see if open ended panel is defined in the course
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel in course_tabs:
- #Add panel to the tabs if it is not defined
+ # Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True
return changed, course_tabs
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index 8762eb3a2a..8a29a637b8 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -12,8 +12,8 @@ from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
-
-from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
+from xmodule.modulestore.exceptions import ItemNotFoundError, \
+ InvalidLocationError
from xmodule.modulestore import Location
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update
@@ -33,9 +33,6 @@ from .component import OPEN_ENDED_COMPONENT_TYPES, \
from django_comment_common.utils import seed_permissions_roles
import datetime
from django.utils.timezone import UTC
-
-# TODO: should explicitly enumerate exports with __all__
-
__all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings',
'course_config_graders_page',
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 25094ddcfe..abc5f48564 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -103,7 +103,7 @@ def clone_item(request):
@expect_json
def delete_item(request):
item_location = request.POST['id']
- item_loc = Location(item_location)
+ item_location = Location(item_location)
# check permissions for this user within this course
if not has_access(request.user, item_location):
@@ -124,11 +124,11 @@ def delete_item(request):
# cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions:
- parent_locs = modulestore('direct').get_parent_locations(item_loc, None)
+ parent_locs = modulestore('direct').get_parent_locations(item_location, None)
for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc)
- item_url = item_loc.url()
+ item_url = item_location.url()
if item_url in parent.children:
children = parent.children
children.remove(item_url)
diff --git a/cms/djangoapps/models/settings/course_details.py b/cms/djangoapps/models/settings/course_details.py
index 07eb4bc309..3f0c87917a 100644
--- a/cms/djangoapps/models/settings/course_details.py
+++ b/cms/djangoapps/models/settings/course_details.py
@@ -41,25 +41,25 @@ class CourseDetails(object):
course.enrollment_start = descriptor.enrollment_start
course.enrollment_end = descriptor.enrollment_end
- temploc = course_location._replace(category='about', name='syllabus')
+ temploc = course_location.replace(category='about', name='syllabus')
try:
course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
- temploc = temploc._replace(name='overview')
+ temploc = temploc.replace(name='overview')
try:
course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
- temploc = temploc._replace(name='effort')
+ temploc = temploc.replace(name='effort')
try:
course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError:
pass
- temploc = temploc._replace(name='video')
+ temploc = temploc.replace(name='video')
try:
raw_video = get_modulestore(temploc).get_item(temploc).data
course.intro_video = CourseDetails.parse_video_tag(raw_video)
@@ -126,16 +126,16 @@ class CourseDetails(object):
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
# to make faster, could compare against db or could have client send over a list of which fields changed.
- temploc = Location(course_location)._replace(category='about', name='syllabus')
+ temploc = Location(course_location).replace(category='about', name='syllabus')
update_item(temploc, jsondict['syllabus'])
- temploc = temploc._replace(name='overview')
+ temploc = temploc.replace(name='overview')
update_item(temploc, jsondict['overview'])
- temploc = temploc._replace(name='effort')
+ temploc = temploc.replace(name='effort')
update_item(temploc, jsondict['effort'])
- temploc = temploc._replace(name='video')
+ temploc = temploc.replace(name='video')
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag)
@@ -174,10 +174,10 @@ class CourseDetails(object):
return result
-# TODO move to a more general util? Is there a better way to do the isinstance model check?
+# TODO move to a more general util?
class CourseSettingsEncoder(json.JSONEncoder):
def default(self, obj):
- if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel):
+ if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)):
return obj.__dict__
elif isinstance(obj, Location):
return obj.dict()
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 8551a56c41..d7c9e6bb90 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -235,8 +235,7 @@ PIPELINE_JS = {
'source_filenames': sorted(
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
- ) + ['js/hesitate.js', 'js/base.js',
- 'js/models/feedback.js', 'js/views/feedback.js',
+ ) + ['js/hesitate.js', 'js/base.js', 'js/views/feedback.js',
'js/models/section.js', 'js/views/section.js',
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/views/assets.js'],
diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json
index 73dfc565a2..5b4d829b3a 100644
--- a/cms/static/coffee/files.json
+++ b/cms/static/coffee/files.json
@@ -7,6 +7,7 @@
"js/vendor/jquery.cookie.js",
"js/vendor/json2.js",
"js/vendor/underscore-min.js",
+ "js/vendor/underscore.string.min.js",
"js/vendor/backbone-min.js",
"js/vendor/jquery.leanModal.min.js",
"js/vendor/sinon-1.7.1.js",
diff --git a/cms/static/coffee/spec/models/feedback_spec.coffee b/cms/static/coffee/spec/models/feedback_spec.coffee
deleted file mode 100644
index 6ddac41ebf..0000000000
--- a/cms/static/coffee/spec/models/feedback_spec.coffee
+++ /dev/null
@@ -1,34 +0,0 @@
-describe "CMS.Models.SystemFeedback", ->
- beforeEach ->
- @model = new CMS.Models.SystemFeedback()
-
- it "should have an empty message by default", ->
- expect(@model.get("message")).toEqual("")
-
- it "should have an empty title by default", ->
- expect(@model.get("title")).toEqual("")
-
- it "should not have an intent set by default", ->
- expect(@model.get("intent")).toBeNull()
-
-
-describe "CMS.Models.WarningMessage", ->
- beforeEach ->
- @model = new CMS.Models.WarningMessage()
-
- it "should have the correct intent", ->
- expect(@model.get("intent")).toEqual("warning")
-
-describe "CMS.Models.ErrorMessage", ->
- beforeEach ->
- @model = new CMS.Models.ErrorMessage()
-
- it "should have the correct intent", ->
- expect(@model.get("intent")).toEqual("error")
-
-describe "CMS.Models.ConfirmationMessage", ->
- beforeEach ->
- @model = new CMS.Models.ConfirmationMessage()
-
- it "should have the correct intent", ->
- expect(@model.get("intent")).toEqual("confirmation")
diff --git a/cms/static/coffee/spec/views/feedback_spec.coffee b/cms/static/coffee/spec/views/feedback_spec.coffee
index 3e7d080a7c..a3950c0b3c 100644
--- a/cms/static/coffee/spec/views/feedback_spec.coffee
+++ b/cms/static/coffee/spec/views/feedback_spec.coffee
@@ -18,79 +18,105 @@ beforeEach ->
else
return trimmedText.indexOf(text) != -1;
-describe "CMS.Views.Alert as base class", ->
+describe "CMS.Views.SystemFeedback", ->
beforeEach ->
- @model = new CMS.Models.ConfirmationMessage({
+ @options =
title: "Portal"
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
- })
# it will be interesting to see when this.render is called, so lets spy on it
- spyOn(CMS.Views.Alert.prototype, 'render').andCallThrough()
+ @renderSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'render').andCallThrough()
+ @showSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'show').andCallThrough()
+ @hideSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'hide').andCallThrough()
- it "renders on initalize", ->
- view = new CMS.Views.Alert({model: @model})
- expect(view.render).toHaveBeenCalled()
+ it "requires a type and an intent", ->
+ neither = =>
+ new CMS.Views.SystemFeedback(@options)
+ noType = =>
+ options = $.extend({}, @options)
+ options.intent = "confirmation"
+ new CMS.Views.SystemFeedback(options)
+ noIntent = =>
+ options = $.extend({}, @options)
+ options.type = "alert"
+ new CMS.Views.SystemFeedback(options)
+ both = =>
+ options = $.extend({}, @options)
+ options.type = "alert"
+ options.intent = "confirmation"
+ new CMS.Views.SystemFeedback(options)
+
+ expect(neither).toThrow()
+ expect(noType).toThrow()
+ expect(noIntent).toThrow()
+ expect(both).not.toThrow()
+
+ # for simplicity, we'll use CMS.Views.Alert.Confirmation from here on,
+ # which extends and proxies to CMS.Views.SystemFeedback
+
+ it "does not show on initalize", ->
+ view = new CMS.Views.Alert.Confirmation(@options)
+ expect(@renderSpy).not.toHaveBeenCalled()
+ expect(@showSpy).not.toHaveBeenCalled()
it "renders the template", ->
- view = new CMS.Views.Alert({model: @model})
+ view = new CMS.Views.Alert.Confirmation(@options)
+ view.show()
+
expect(view.$(".action-close")).toBeDefined()
expect(view.$('.wrapper')).toBeShown()
- expect(view.$el).toContainText(@model.get("title"))
- expect(view.$el).toContainText(@model.get("message"))
+ expect(view.$el).toContainText(@options.title)
+ expect(view.$el).toContainText(@options.message)
it "close button sends a .hide() message", ->
- spyOn(CMS.Views.Alert.prototype, 'hide').andCallThrough()
-
- view = new CMS.Views.Alert({model: @model})
+ view = new CMS.Views.Alert.Confirmation(@options).show()
view.$(".action-close").click()
- expect(CMS.Views.Alert.prototype.hide).toHaveBeenCalled()
+ expect(@hideSpy).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeHiding()
describe "CMS.Views.Prompt", ->
- beforeEach ->
- @model = new CMS.Models.ConfirmationMessage({
- title: "Portal"
- message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
- })
-
# for some reason, expect($("body")) blows up the test runner, so this test
# just exercises the Prompt rather than asserting on anything. Best I can
# do for now. :(
it "changes class on body", ->
# expect($("body")).not.toHaveClass("prompt-is-shown")
- view = new CMS.Views.Prompt({model: @model})
+ view = new CMS.Views.Prompt.Confirmation({
+ title: "Portal"
+ message: "Welcome to the Aperture Science Computer-Aided Enrichment Center"
+ })
# expect($("body")).toHaveClass("prompt-is-shown")
view.hide()
# expect($("body")).not.toHaveClass("prompt-is-shown")
-describe "CMS.Views.Alert click events", ->
+describe "CMS.Views.SystemFeedback click events", ->
beforeEach ->
- @model = new CMS.Models.WarningMessage(
+ @primaryClickSpy = jasmine.createSpy('primaryClick')
+ @secondaryClickSpy = jasmine.createSpy('secondaryClick')
+ @view = new CMS.Views.Notification.Warning(
title: "Unsaved",
message: "Your content is currently Unsaved.",
actions:
primary:
text: "Save",
class: "save-button",
- click: jasmine.createSpy('primaryClick')
+ click: @primaryClickSpy
secondary: [{
text: "Revert",
class: "cancel-button",
- click: jasmine.createSpy('secondaryClick')
+ click: @secondaryClickSpy
}]
-
)
-
- @view = new CMS.Views.Alert({model: @model})
+ @view.show()
it "should trigger the primary event on a primary click", ->
- @view.primaryClick()
- expect(@model.get('actions').primary.click).toHaveBeenCalled()
+ @view.$(".action-primary").click()
+ expect(@primaryClickSpy).toHaveBeenCalled()
+ expect(@secondaryClickSpy).not.toHaveBeenCalled()
it "should trigger the secondary event on a secondary click", ->
- @view.secondaryClick()
- expect(@model.get('actions').secondary[0].click).toHaveBeenCalled()
+ @view.$(".action-secondary").click()
+ expect(@secondaryClickSpy).toHaveBeenCalled()
+ expect(@primaryClickSpy).not.toHaveBeenCalled()
it "should apply class to primary action", ->
expect(@view.$(".action-primary")).toHaveClass("save-button")
@@ -100,20 +126,18 @@ describe "CMS.Views.Alert click events", ->
describe "CMS.Views.Notification minShown and maxShown", ->
beforeEach ->
- @model = new CMS.Models.SystemFeedback(
- intent: "saving"
- title: "Saving"
- )
- spyOn(CMS.Views.Notification.prototype, 'show').andCallThrough()
- spyOn(CMS.Views.Notification.prototype, 'hide').andCallThrough()
+ @showSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'show')
+ @showSpy.andCallThrough()
+ @hideSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'hide')
+ @hideSpy.andCallThrough()
@clock = sinon.useFakeTimers()
afterEach ->
@clock.restore()
it "a minShown view should not hide too quickly", ->
- view = new CMS.Views.Notification({model: @model, minShown: 1000})
- expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
+ view = new CMS.Views.Notification.Saving({minShown: 1000})
+ view.show()
expect(view.$('.wrapper')).toBeShown()
# call hide() on it, but the minShown should prevent it from hiding right away
@@ -125,8 +149,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view should hide by itself", ->
- view = new CMS.Views.Notification({model: @model, maxShown: 1000})
- expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
+ view = new CMS.Views.Notification.Saving({maxShown: 1000})
+ view.show()
expect(view.$('.wrapper')).toBeShown()
# wait for the maxShown timeout to expire, and check again
@@ -134,13 +158,13 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding()
it "a minShown view can stay visible longer", ->
- view = new CMS.Views.Notification({model: @model, minShown: 1000})
- expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
+ view = new CMS.Views.Notification.Saving({minShown: 1000})
+ view.show()
expect(view.$('.wrapper')).toBeShown()
# wait for the minShown timeout to expire, and check again
@clock.tick(1001)
- expect(CMS.Views.Notification.prototype.hide).not.toHaveBeenCalled()
+ expect(@hideSpy).not.toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown()
# can now hide immediately
@@ -148,8 +172,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view can hide early", ->
- view = new CMS.Views.Notification({model: @model, maxShown: 1000})
- expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled()
+ view = new CMS.Views.Notification.Saving({maxShown: 1000})
+ view.show()
expect(view.$('.wrapper')).toBeShown()
# wait 50 milliseconds, and hide it early
@@ -162,7 +186,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding()
it "a view can have both maxShown and minShown", ->
- view = new CMS.Views.Notification({model: @model, minShown: 1000, maxShown: 2000})
+ view = new CMS.Views.Notification.Saving({minShown: 1000, maxShown: 2000})
+ view.show()
# can't hide early
@clock.tick(50)
diff --git a/cms/static/coffee/src/main.coffee b/cms/static/coffee/src/main.coffee
index efcd869113..8043b41638 100644
--- a/cms/static/coffee/src/main.coffee
+++ b/cms/static/coffee/src/main.coffee
@@ -18,11 +18,15 @@ $ ->
$(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) ->
if ajaxSettings.notifyOnError is false
return
- msg = new CMS.Models.ErrorMessage(
+ if jqXHR.responseText
+ message = _.str.truncate(jqXHR.responseText, 300)
+ else
+ message = gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
+ msg = new CMS.Views.Notification.Error(
"title": gettext("Studio's having trouble saving your work")
- "message": jqXHR.responseText || gettext("This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.")
+ "message": message
)
- new CMS.Views.Notification({model: msg})
+ msg.show()
window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i
diff --git a/cms/static/js/models/feedback.js b/cms/static/js/models/feedback.js
deleted file mode 100644
index d57cffa779..0000000000
--- a/cms/static/js/models/feedback.js
+++ /dev/null
@@ -1,55 +0,0 @@
-CMS.Models.SystemFeedback = Backbone.Model.extend({
- defaults: {
- "intent": null, // "warning", "confirmation", "error", "announcement", "step-required", etc
- "title": "",
- "message": ""
- /* could also have an "actions" hash: here is an example demonstrating
- the expected structure
- "actions": {
- "primary": {
- "text": "Save",
- "class": "action-save",
- "click": function() {
- // do something when Save is clicked
- // `this` refers to the model
- }
- },
- "secondary": [
- {
- "text": "Cancel",
- "class": "action-cancel",
- "click": function() {}
- }, {
- "text": "Discard Changes",
- "class": "action-discard",
- "click": function() {}
- }
- ]
- }
- */
- }
-});
-
-CMS.Models.WarningMessage = CMS.Models.SystemFeedback.extend({
- defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
- "intent": "warning"
- })
-});
-
-CMS.Models.ErrorMessage = CMS.Models.SystemFeedback.extend({
- defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
- "intent": "error"
- })
-});
-
-CMS.Models.ConfirmAssetDeleteMessage = CMS.Models.SystemFeedback.extend({
- defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
- "intent": "warning"
- })
-});
-
-CMS.Models.ConfirmationMessage = CMS.Models.SystemFeedback.extend({
- defaults: $.extend({}, CMS.Models.SystemFeedback.prototype.defaults, {
- "intent": "confirmation"
- })
-});
diff --git a/cms/static/js/models/section.js b/cms/static/js/models/section.js
index 467a2709a6..902585c58c 100644
--- a/cms/static/js/models/section.js
+++ b/cms/static/js/models/section.js
@@ -22,22 +22,16 @@ CMS.Models.Section = Backbone.Model.extend({
},
showNotification: function() {
if(!this.msg) {
- this.msg = new CMS.Models.SystemFeedback({
- intent: "saving",
- title: gettext("Saving…")
- });
- }
- if(!this.msgView) {
- this.msgView = new CMS.Views.Notification({
- model: this.msg,
+ this.msg = new CMS.Views.Notification.Saving({
+ title: gettext("Saving…"),
closeIcon: false,
minShown: 1250
});
}
- this.msgView.show();
+ this.msg.show();
},
hideNotification: function() {
- if(!this.msgView) { return; }
- this.msgView.hide();
+ if(!this.msg) { return; }
+ this.msg.hide();
}
});
diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js
index 1a1a33ec1b..b04fb6e3d1 100644
--- a/cms/static/js/views/feedback.js
+++ b/cms/static/js/views/feedback.js
@@ -1,39 +1,64 @@
-CMS.Views.Alert = Backbone.View.extend({
+CMS.Views.SystemFeedback = Backbone.View.extend({
options: {
- type: "alert",
+ title: "",
+ message: "",
+ intent: null, // "warning", "confirmation", "error", "announcement", "step-required", etc
+ type: null, // "alert", "notification", or "prompt": set by subclass
shown: true, // is this view currently being shown?
icon: true, // should we render an icon related to the message intent?
closeIcon: true, // should we render a close button in the top right corner?
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds)
+
+ /* could also have an "actions" hash: here is an example demonstrating
+ the expected structure
+ actions: {
+ primary: {
+ "text": "Save",
+ "class": "action-save",
+ "click": function(view) {
+ // do something when Save is clicked
+ }
+ },
+ secondary: [
+ {
+ "text": "Cancel",
+ "class": "action-cancel",
+ "click": function(view) {}
+ }, {
+ "text": "Discard Changes",
+ "class": "action-discard",
+ "click": function(view) {}
+ }
+ ]
+ }
+ */
},
initialize: function() {
+ if(!this.options.type) {
+ throw "SystemFeedback: type required (given " +
+ JSON.stringify(this.options) + ")";
+ }
+ if(!this.options.intent) {
+ throw "SystemFeedback: intent required (given " +
+ JSON.stringify(this.options) + ")";
+ }
var tpl = $("#system-feedback-tpl").text();
if(!tpl) {
console.error("Couldn't load system-feedback template");
}
this.template = _.template(tpl);
this.setElement($("#page-"+this.options.type));
- this.listenTo(this.model, 'change', this.render);
- return this.show();
- },
- render: function() {
- var attrs = $.extend({}, this.options, this.model.attributes);
- this.$el.html(this.template(attrs));
return this;
},
- events: {
- "click .action-close": "hide",
- "click .action-primary": "primaryClick",
- "click .action-secondary": "secondaryClick"
- },
+ // public API: show() and hide()
show: function() {
clearTimeout(this.hideTimeout);
this.options.shown = true;
this.shownAt = new Date();
this.render();
if($.isNumeric(this.options.maxShown)) {
- this.hideTimeout = setTimeout($.proxy(this.hide, this),
+ this.hideTimeout = setTimeout(_.bind(this.hide, this),
this.options.maxShown);
}
return this;
@@ -43,7 +68,7 @@ CMS.Views.Alert = Backbone.View.extend({
this.options.minShown > new Date() - this.shownAt)
{
clearTimeout(this.hideTimeout);
- this.hideTimeout = setTimeout($.proxy(this.hide, this),
+ this.hideTimeout = setTimeout(_.bind(this.hide, this),
this.options.minShown - (new Date() - this.shownAt));
} else {
this.options.shown = false;
@@ -52,40 +77,63 @@ CMS.Views.Alert = Backbone.View.extend({
}
return this;
},
- primaryClick: function() {
- var actions = this.model.get("actions");
+ // the rest of the API should be considered semi-private
+ events: {
+ "click .action-close": "hide",
+ "click .action-primary": "primaryClick",
+ "click .action-secondary": "secondaryClick"
+ },
+ render: function() {
+ // there can be only one active view of a given type at a time: only
+ // one alert, only one notification, only one prompt. Therefore, we'll
+ // use a singleton approach.
+ var parent = CMS.Views[_.str.capitalize(this.options.type)];
+ if(parent && parent.active && parent.active !== this) {
+ parent.active.stopListening();
+ }
+ this.$el.html(this.template(this.options));
+ parent.active = this;
+ return this;
+ },
+ primaryClick: function(event) {
+ var actions = this.options.actions;
if(!actions) { return; }
var primary = actions.primary;
if(!primary) { return; }
if(primary.click) {
- primary.click.call(this.model, this);
+ primary.click.call(event.target, this, event);
}
},
- secondaryClick: function(e) {
- var actions = this.model.get("actions");
+ secondaryClick: function(event) {
+ var actions = this.options.actions;
if(!actions) { return; }
var secondaryList = actions.secondary;
if(!secondaryList) { return; }
// which secondary action was clicked?
var i = 0; // default to the first secondary action (easier for testing)
- if(e && e.target) {
- i = _.indexOf(this.$(".action-secondary"), e.target);
+ if(event && event.target) {
+ i = _.indexOf(this.$(".action-secondary"), event.target);
}
- var secondary = this.model.get("actions").secondary[i];
+ var secondary = secondaryList[i];
if(secondary.click) {
- secondary.click.call(this.model, this);
+ secondary.click.call(event.target, this, event);
}
}
});
-CMS.Views.Notification = CMS.Views.Alert.extend({
- options: $.extend({}, CMS.Views.Alert.prototype.options, {
+CMS.Views.Alert = CMS.Views.SystemFeedback.extend({
+ options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
+ type: "alert"
+ })
+});
+CMS.Views.Notification = CMS.Views.SystemFeedback.extend({
+ options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
type: "notification",
closeIcon: false
})
});
-CMS.Views.Prompt = CMS.Views.Alert.extend({
- options: $.extend({}, CMS.Views.Alert.prototype.options, {
+CMS.Views.Prompt = CMS.Views.SystemFeedback.extend({
+ options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
type: "prompt",
closeIcon: false,
icon: false
@@ -98,6 +146,27 @@ CMS.Views.Prompt = CMS.Views.Alert.extend({
$body.removeClass('prompt-is-shown');
}
// super() in Javascript has awkward syntax :(
- return CMS.Views.Alert.prototype.render.apply(this, arguments);
+ return CMS.Views.SystemFeedback.prototype.render.apply(this, arguments);
}
});
+
+// create CMS.Views.Alert.Warning, CMS.Views.Notification.Confirmation,
+// CMS.Views.Prompt.StepRequired, etc
+var capitalCamel, types, intents;
+capitalCamel = _.compose(_.str.capitalize, _.str.camelize);
+types = ["alert", "notification", "prompt"];
+intents = ["warning", "error", "confirmation", "announcement", "step-required", "help", "saving"];
+_.each(types, function(type) {
+ _.each(intents, function(intent) {
+ // "class" is a reserved word in Javascript, so use "klass" instead
+ var klass, subklass;
+ klass = CMS.Views[capitalCamel(type)];
+ subklass = klass.extend({
+ options: $.extend({}, klass.prototype.options, {
+ type: type,
+ intent: intent
+ })
+ });
+ klass[capitalCamel(intent)] = subklass;
+ });
+});
diff --git a/cms/static/js/views/section.js b/cms/static/js/views/section.js
index 622249414d..eccc547a06 100644
--- a/cms/static/js/views/section.js
+++ b/cms/static/js/views/section.js
@@ -67,7 +67,7 @@ CMS.Views.SectionEdit = Backbone.View.extend({
showInvalidMessage: function(model, error, options) {
model.set("name", model.previous("name"));
var that = this;
- var msg = new CMS.Models.ErrorMessage({
+ var prompt = new CMS.Views.Prompt.Error({
title: gettext("Your change could not be saved"),
message: error,
actions: {
@@ -80,6 +80,6 @@ CMS.Views.SectionEdit = Backbone.View.extend({
}
}
});
- new CMS.Views.Prompt({model: msg});
+ prompt.show();
}
});
diff --git a/cms/templates/asset_index.html b/cms/templates/asset_index.html
index e8dc523ba7..0006d29d38 100644
--- a/cms/templates/asset_index.html
+++ b/cms/templates/asset_index.html
@@ -8,7 +8,6 @@
<%block name="jsextra">
-
+
@@ -54,7 +55,6 @@
-
diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py
index b843d47934..34c0154449 100644
--- a/common/djangoapps/terrain/course_helpers.py
+++ b/common/djangoapps/terrain/course_helpers.py
@@ -1,5 +1,5 @@
-#pylint: disable=C0111
-#pylint: disable=W0621
+# pylint: disable=C0111
+# pylint: disable=W0621
from lettuce import world, step
from .factories import *
diff --git a/common/djangoapps/track/views.py b/common/djangoapps/track/views.py
index b2935a6a89..221bab5468 100644
--- a/common/djangoapps/track/views.py
+++ b/common/djangoapps/track/views.py
@@ -1,13 +1,11 @@
import json
import logging
-import os
import pytz
import datetime
import dateutil.parser
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
-from django.http import Http404
from django.shortcuts import redirect
from django.conf import settings
from mitxmako.shortcuts import render_to_response
@@ -22,6 +20,7 @@ LOGFIELDS = ['username', 'ip', 'event_source', 'event_type', 'event', 'agent', '
def log_event(event):
+ """Write tracking event to log file, and optionally to TrackingLog model."""
event_str = json.dumps(event)
log.info(event_str[:settings.TRACK_MAX_EVENT])
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
@@ -34,6 +33,11 @@ def log_event(event):
def user_track(request):
+ """
+ Log when GET call to "event" URL is made by a user.
+
+ GET call should provide "event_type", "event", and "page" arguments.
+ """
try: # TODO: Do the same for many of the optional META parameters
username = request.user.username
except:
@@ -50,7 +54,6 @@ def user_track(request):
except:
agent = ''
- # TODO: Move a bunch of this into log_event
event = {
"username": username,
"session": scookie,
@@ -68,6 +71,7 @@ def user_track(request):
def server_track(request, event_type, event, page=None):
+ """Log events related to server requests."""
try:
username = request.user.username
except:
@@ -95,9 +99,52 @@ def server_track(request, event_type, event, page=None):
log_event(event)
+def task_track(request_info, task_info, event_type, event, page=None):
+ """
+ Logs tracking information for events occuring within celery tasks.
+
+ The `event_type` is a string naming the particular event being logged,
+ while `event` is a dict containing whatever additional contextual information
+ is desired.
+
+ The `request_info` is a dict containing information about the original
+ task request. Relevant keys are `username`, `ip`, `agent`, and `host`.
+ While the dict is required, the values in it are not, so that {} can be
+ passed in.
+
+ In addition, a `task_info` dict provides more information about the current
+ task, to be stored with the `event` dict. This may also be an empty dict.
+
+ The `page` parameter is optional, and allows the name of the page to
+ be provided.
+ """
+
+ # supplement event information with additional information
+ # about the task in which it is running.
+ full_event = dict(event, **task_info)
+
+ # All fields must be specified, in case the tracking information is
+ # also saved to the TrackingLog model. Get values from the task-level
+ # information, or just add placeholder values.
+ event = {
+ "username": request_info.get('username', 'unknown'),
+ "ip": request_info.get('ip', 'unknown'),
+ "event_source": "task",
+ "event_type": event_type,
+ "event": full_event,
+ "agent": request_info.get('agent', 'unknown'),
+ "page": page,
+ "time": datetime.datetime.utcnow().isoformat(),
+ "host": request_info.get('host', 'unknown')
+ }
+
+ log_event(event)
+
+
@login_required
@ensure_csrf_cookie
def view_tracking_log(request, args=''):
+ """View to output contents of TrackingLog model. For staff use only."""
if not request.user.is_staff:
return redirect('/')
nlen = 100
diff --git a/common/djangoapps/xmodule_modifiers.py b/common/djangoapps/xmodule_modifiers.py
index 570b38c942..7a74e75591 100644
--- a/common/djangoapps/xmodule_modifiers.py
+++ b/common/djangoapps/xmodule_modifiers.py
@@ -1,4 +1,3 @@
-import re
import json
import logging
import static_replace
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index 7dcd7b925e..2a9f3d82a3 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -15,25 +15,22 @@ This is used by capa_module.
from datetime import datetime
import logging
-import math
-import numpy
import os.path
import re
-import sys
from lxml import etree
from xml.sax.saxutils import unescape
from copy import deepcopy
-from .correctmap import CorrectMap
-import inputtypes
-import customrender
-from .util import contextualize_text, convert_files_to_filenames
-import xqueue_interface
+from capa.correctmap import CorrectMap
+import capa.inputtypes as inputtypes
+import capa.customrender as customrender
+from capa.util import contextualize_text, convert_files_to_filenames
+import capa.xqueue_interface as xqueue_interface
# to be replaced with auto-registering
-import responsetypes
-import safe_exec
+import capa.responsetypes as responsetypes
+from capa.safe_exec import safe_exec
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
@@ -46,8 +43,8 @@ response_properties = ["codeparam", "responseparam", "answer", "openendedparam"]
# special problem tags which should be turned into innocuous HTML
html_transforms = {'problem': {'tag': 'div'},
- "text": {'tag': 'span'},
- "math": {'tag': 'span'},
+ 'text': {'tag': 'span'},
+ 'math': {'tag': 'span'},
}
# These should be removed from HTML output, including all subelements
@@ -134,7 +131,6 @@ class LoncapaProblem(object):
self.extracted_tree = self._extract_html(self.tree)
-
def do_reset(self):
'''
Reset internal state to unfinished, with no answers
@@ -175,7 +171,7 @@ class LoncapaProblem(object):
Return the maximum score for this problem.
'''
maxscore = 0
- for response, responder in self.responders.iteritems():
+ for responder in self.responders.values():
maxscore += responder.get_max_score()
return maxscore
@@ -220,7 +216,7 @@ class LoncapaProblem(object):
def ungraded_response(self, xqueue_msg, queuekey):
'''
Handle any responses from the xqueue that do not contain grades
- Will try to pass the queue message to all inputtypes that can handle ungraded responses
+ Will try to pass the queue message to all inputtypes that can handle ungraded responses
Does not return any value
'''
@@ -230,7 +226,6 @@ class LoncapaProblem(object):
if hasattr(the_input, 'ungraded_response'):
the_input.ungraded_response(xqueue_msg, queuekey)
-
def is_queued(self):
'''
Returns True if any part of the problem has been submitted to an external queue
@@ -238,7 +233,6 @@ class LoncapaProblem(object):
'''
return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map)
-
def get_recentmost_queuetime(self):
'''
Returns a DateTime object that represents the timestamp of the most recent
@@ -256,11 +250,11 @@ class LoncapaProblem(object):
return max(queuetimes)
-
def grade_answers(self, answers):
'''
Grade student responses. Called by capa_module.check_problem.
- answers is a dict of all the entries from request.POST, but with the first part
+
+ `answers` is a dict of all the entries from request.POST, but with the first part
of each key removed (the string before the first "_").
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
@@ -270,24 +264,72 @@ class LoncapaProblem(object):
# if answers include File objects, convert them to filenames.
self.student_answers = convert_files_to_filenames(answers)
+ return self._grade_answers(answers)
+ def supports_rescoring(self):
+ """
+ Checks that the current problem definition permits rescoring.
+
+ More precisely, it checks that there are no response types in
+ the current problem that are not fully supported (yet) for rescoring.
+
+ This includes responsetypes for which the student's answer
+ is not properly stored in state, i.e. file submissions. At present,
+ we have no way to know if an existing response was actually a real
+ answer or merely the filename of a file submitted as an answer.
+
+ It turns out that because rescoring is a background task, limiting
+ it to responsetypes that don't support file submissions also means
+ that the responsetypes are synchronous. This is convenient as it
+ permits rescoring to be complete when the rescoring call returns.
+ """
+ return all('filesubmission' not in responder.allowed_inputfields for responder in self.responders.values())
+
+ def rescore_existing_answers(self):
+ """
+ Rescore student responses. Called by capa_module.rescore_problem.
+ """
+ return self._grade_answers(None)
+
+ def _grade_answers(self, student_answers):
+ """
+ Internal grading call used for checking new 'student_answers' and also
+ rescoring existing student_answers.
+
+ For new student_answers being graded, `student_answers` is a dict of all the
+ entries from request.POST, but with the first part of each key removed
+ (the string before the first "_"). Thus, for example,
+ input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123.
+
+ For rescoring, `student_answers` is None.
+
+ Calls the Response for each question in this problem, to do the actual grading.
+ """
# old CorrectMap
oldcmap = self.correct_map
# start new with empty CorrectMap
newcmap = CorrectMap()
- # log.debug('Responders: %s' % self.responders)
+
# Call each responsetype instance to do actual grading
for responder in self.responders.values():
- # File objects are passed only if responsetype explicitly allows for file
- # submissions
- if 'filesubmission' in responder.allowed_inputfields:
- results = responder.evaluate_answers(answers, oldcmap)
+ # File objects are passed only if responsetype explicitly allows
+ # for file submissions. But we have no way of knowing if
+ # student_answers contains a proper answer or the filename of
+ # an earlier submission, so for now skip these entirely.
+ # TODO: figure out where to get file submissions when rescoring.
+ if 'filesubmission' in responder.allowed_inputfields and student_answers is None:
+ raise Exception("Cannot rescore problems with possible file submissions")
+
+ # use 'student_answers' only if it is provided, and if it might contain a file
+ # submission that would not exist in the persisted "student_answers".
+ if 'filesubmission' in responder.allowed_inputfields and student_answers is not None:
+ results = responder.evaluate_answers(student_answers, oldcmap)
else:
- results = responder.evaluate_answers(convert_files_to_filenames(answers), oldcmap)
+ results = responder.evaluate_answers(self.student_answers, oldcmap)
newcmap.update(results)
+
self.correct_map = newcmap
- # log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap))
return newcmap
def get_question_answers(self):
@@ -331,7 +373,6 @@ class LoncapaProblem(object):
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
return html
-
def handle_input_ajax(self, get):
'''
InputTypes can support specialized AJAX calls. Find the correct input and pass along the correct data
@@ -348,8 +389,6 @@ class LoncapaProblem(object):
log.warning("Could not find matching input for id: %s" % input_id)
return {}
-
-
# ======= Private Methods Below ========
def _process_includes(self):
@@ -359,16 +398,16 @@ class LoncapaProblem(object):
'''
includes = self.tree.findall('.//include')
for inc in includes:
- file = inc.get('file')
- if file is not None:
+ filename = inc.get('file')
+ if filename is not None:
try:
# open using ModuleSystem OSFS filestore
- ifp = self.system.filestore.open(file)
+ ifp = self.system.filestore.open(filename)
except Exception as err:
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
log.warning('Cannot find file %s in %s' % (
- file, self.system.filestore))
+ filename, self.system.filestore))
# if debugging, don't fail - just log error
# TODO (vshnayder): need real error handling, display to users
if not self.system.get('DEBUG'):
@@ -381,7 +420,7 @@ class LoncapaProblem(object):
except Exception as err:
log.warning('Error %s in problem xml include: %s' % (
err, etree.tostring(inc, pretty_print=True)))
- log.warning('Cannot parse XML in %s' % (file))
+ log.warning('Cannot parse XML in %s' % (filename))
# if debugging, don't fail - just log error
# TODO (vshnayder): same as above
if not self.system.get('DEBUG'):
@@ -389,11 +428,11 @@ class LoncapaProblem(object):
else:
continue
- # insert new XML into tree in place of inlcude
+ # insert new XML into tree in place of include
parent = inc.getparent()
parent.insert(parent.index(inc), incxml)
parent.remove(inc)
- log.debug('Included %s into %s' % (file, self.problem_id))
+ log.debug('Included %s into %s' % (filename, self.problem_id))
def _extract_system_path(self, script):
"""
@@ -463,7 +502,7 @@ class LoncapaProblem(object):
if all_code:
try:
- safe_exec.safe_exec(
+ safe_exec(
all_code,
context,
random_seed=self.seed,
@@ -519,18 +558,18 @@ class LoncapaProblem(object):
value = ""
if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemid]
-
+
if input_id not in self.input_state:
self.input_state[input_id] = {}
-
+
# do the rendering
state = {'value': value,
- 'status': status,
- 'id': input_id,
- 'input_state': self.input_state[input_id],
- 'feedback': {'message': msg,
- 'hint': hint,
- 'hintmode': hintmode, }}
+ 'status': status,
+ 'id': input_id,
+ 'input_state': self.input_state[input_id],
+ 'feedback': {'message': msg,
+ 'hint': hint,
+ 'hintmode': hintmode, }}
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
# save the input type so that we can make ajax calls on it if we need to
@@ -554,7 +593,7 @@ class LoncapaProblem(object):
for item in problemtree:
item_xhtml = self._extract_html(item)
if item_xhtml is not None:
- tree.append(item_xhtml)
+ tree.append(item_xhtml)
if tree.tag in html_transforms:
tree.tag = html_transforms[problemtree.tag]['tag']
diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py
index 20de19f567..68be54b6af 100644
--- a/common/lib/capa/capa/tests/test_responsetypes.py
+++ b/common/lib/capa/capa/tests/test_responsetypes.py
@@ -4,7 +4,6 @@ Tests of responsetypes
from datetime import datetime
import json
-from nose.plugins.skip import SkipTest
import os
import random
import unittest
@@ -56,9 +55,18 @@ class ResponseTest(unittest.TestCase):
self.assertEqual(result, 'incorrect',
msg="%s should be marked incorrect" % str(input_str))
+ def _get_random_number_code(self):
+ """Returns code to be used to generate a random result."""
+ return "str(random.randint(0, 1e9))"
+
+ def _get_random_number_result(self, seed_value):
+ """Returns a result that should be generated using the random_number_code."""
+ rand = random.Random(seed_value)
+ return str(rand.randint(0, 1e9))
+
class MultiChoiceResponseTest(ResponseTest):
- from response_xml_factory import MultipleChoiceResponseXMLFactory
+ from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
xml_factory_class = MultipleChoiceResponseXMLFactory
def test_multiple_choice_grade(self):
@@ -80,7 +88,7 @@ class MultiChoiceResponseTest(ResponseTest):
class TrueFalseResponseTest(ResponseTest):
- from response_xml_factory import TrueFalseResponseXMLFactory
+ from capa.tests.response_xml_factory import TrueFalseResponseXMLFactory
xml_factory_class = TrueFalseResponseXMLFactory
def test_true_false_grade(self):
@@ -120,7 +128,7 @@ class TrueFalseResponseTest(ResponseTest):
class ImageResponseTest(ResponseTest):
- from response_xml_factory import ImageResponseXMLFactory
+ from capa.tests.response_xml_factory import ImageResponseXMLFactory
xml_factory_class = ImageResponseXMLFactory
def test_rectangle_grade(self):
@@ -184,7 +192,7 @@ class ImageResponseTest(ResponseTest):
class SymbolicResponseTest(ResponseTest):
- from response_xml_factory import SymbolicResponseXMLFactory
+ from capa.tests.response_xml_factory import SymbolicResponseXMLFactory
xml_factory_class = SymbolicResponseXMLFactory
def test_grade_single_input(self):
@@ -224,8 +232,8 @@ class SymbolicResponseTest(ResponseTest):
def test_complex_number_grade(self):
problem = self.build_problem(math_display=True,
- expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
- options=["matrix", "imaginary"])
+ expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
+ options=["matrix", "imaginary"])
# For LaTeX-style inputs, symmath_check() will try to contact
# a server to convert the input to MathML.
@@ -312,16 +320,16 @@ class SymbolicResponseTest(ResponseTest):
# Should not allow multiple inputs, since we specify
# only one "expect" value
with self.assertRaises(Exception):
- problem = self.build_problem(math_display=True,
- expect="2*x+3*y",
- num_inputs=3)
+ self.build_problem(math_display=True,
+ expect="2*x+3*y",
+ num_inputs=3)
def _assert_symbolic_grade(self, problem,
- student_input,
- dynamath_input,
- expected_correctness):
+ student_input,
+ dynamath_input,
+ expected_correctness):
input_dict = {'1_2_1': str(student_input),
- '1_2_1_dynamath': str(dynamath_input)}
+ '1_2_1_dynamath': str(dynamath_input)}
correct_map = problem.grade_answers(input_dict)
@@ -330,7 +338,7 @@ class SymbolicResponseTest(ResponseTest):
class OptionResponseTest(ResponseTest):
- from response_xml_factory import OptionResponseXMLFactory
+ from capa.tests.response_xml_factory import OptionResponseXMLFactory
xml_factory_class = OptionResponseXMLFactory
def test_grade(self):
@@ -350,7 +358,7 @@ class FormulaResponseTest(ResponseTest):
"""
Test the FormulaResponse class
"""
- from response_xml_factory import FormulaResponseXMLFactory
+ from capa.tests.response_xml_factory import FormulaResponseXMLFactory
xml_factory_class = FormulaResponseXMLFactory
def test_grade(self):
@@ -570,7 +578,7 @@ class FormulaResponseTest(ResponseTest):
class StringResponseTest(ResponseTest):
- from response_xml_factory import StringResponseXMLFactory
+ from capa.tests.response_xml_factory import StringResponseXMLFactory
xml_factory_class = StringResponseXMLFactory
def test_case_sensitive(self):
@@ -647,19 +655,18 @@ class StringResponseTest(ResponseTest):
hintfn="gimme_a_random_hint",
script=textwrap.dedent("""
def gimme_a_random_hint(answer_ids, student_answers, new_cmap, old_cmap):
- answer = str(random.randint(0, 1e9))
+ answer = {code}
new_cmap.set_hint_and_mode(answer_ids[0], answer, "always")
- """)
+ """.format(code=self._get_random_number_code()))
)
correct_map = problem.grade_answers({'1_2_1': '2'})
hint = correct_map.get_hint('1_2_1')
- r = random.Random(problem.seed)
- self.assertEqual(hint, str(r.randint(0, 1e9)))
+ self.assertEqual(hint, self._get_random_number_result(problem.seed))
class CodeResponseTest(ResponseTest):
- from response_xml_factory import CodeResponseXMLFactory
+ from capa.tests.response_xml_factory import CodeResponseXMLFactory
xml_factory_class = CodeResponseXMLFactory
def setUp(self):
@@ -673,6 +680,7 @@ class CodeResponseTest(ResponseTest):
@staticmethod
def make_queuestate(key, time):
+ """Create queuestate dict"""
timestr = datetime.strftime(time, dateformat)
return {'key': key, 'time': timestr}
@@ -710,7 +718,7 @@ class CodeResponseTest(ResponseTest):
old_cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i
- queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now())
+ queuestate = CodeResponseTest.make_queuestate(queuekey, datetime.now())
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
# Message format common to external graders
@@ -771,7 +779,7 @@ class CodeResponseTest(ResponseTest):
for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i
latest_timestamp = datetime.now()
- queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp)
+ queuestate = CodeResponseTest.make_queuestate(queuekey, latest_timestamp)
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
self.problem.correct_map.update(cmap)
@@ -796,7 +804,7 @@ class CodeResponseTest(ResponseTest):
class ChoiceResponseTest(ResponseTest):
- from response_xml_factory import ChoiceResponseXMLFactory
+ from capa.tests.response_xml_factory import ChoiceResponseXMLFactory
xml_factory_class = ChoiceResponseXMLFactory
def test_radio_group_grade(self):
@@ -828,7 +836,7 @@ class ChoiceResponseTest(ResponseTest):
class JavascriptResponseTest(ResponseTest):
- from response_xml_factory import JavascriptResponseXMLFactory
+ from capa.tests.response_xml_factory import JavascriptResponseXMLFactory
xml_factory_class = JavascriptResponseXMLFactory
def test_grade(self):
@@ -858,7 +866,7 @@ class JavascriptResponseTest(ResponseTest):
system.can_execute_unsafe_code = lambda: False
with self.assertRaises(LoncapaProblemError):
- problem = self.build_problem(
+ self.build_problem(
system=system,
generator_src="test_problem_generator.js",
grader_src="test_problem_grader.js",
@@ -869,7 +877,7 @@ class JavascriptResponseTest(ResponseTest):
class NumericalResponseTest(ResponseTest):
- from response_xml_factory import NumericalResponseXMLFactory
+ from capa.tests.response_xml_factory import NumericalResponseXMLFactory
xml_factory_class = NumericalResponseXMLFactory
def test_grade_exact(self):
@@ -961,7 +969,7 @@ class NumericalResponseTest(ResponseTest):
class CustomResponseTest(ResponseTest):
- from response_xml_factory import CustomResponseXMLFactory
+ from capa.tests.response_xml_factory import CustomResponseXMLFactory
xml_factory_class = CustomResponseXMLFactory
def test_inline_code(self):
@@ -1000,15 +1008,14 @@ class CustomResponseTest(ResponseTest):
def test_inline_randomization(self):
# Make sure the seed from the problem gets fed into the script execution.
- inline_script = """messages[0] = str(random.randint(0, 1e9))"""
+ inline_script = "messages[0] = {code}".format(code=self._get_random_number_code())
problem = self.build_problem(answer=inline_script)
input_dict = {'1_2_1': '0'}
correctmap = problem.grade_answers(input_dict)
input_msg = correctmap.get_msg('1_2_1')
- r = random.Random(problem.seed)
- self.assertEqual(input_msg, str(r.randint(0, 1e9)))
+ self.assertEqual(input_msg, self._get_random_number_result(problem.seed))
def test_function_code_single_input(self):
# For function code, we pass in these arguments:
@@ -1241,25 +1248,23 @@ class CustomResponseTest(ResponseTest):
def test_setup_randomization(self):
# Ensure that the problem setup script gets the random seed from the problem.
script = textwrap.dedent("""
- num = random.randint(0, 1e9)
- """)
+ num = {code}
+ """.format(code=self._get_random_number_code()))
problem = self.build_problem(script=script)
- r = random.Random(problem.seed)
- self.assertEqual(r.randint(0, 1e9), problem.context['num'])
+ self.assertEqual(problem.context['num'], self._get_random_number_result(problem.seed))
def test_check_function_randomization(self):
# The check function should get random-seeded from the problem.
script = textwrap.dedent("""
def check_func(expect, answer_given):
- return {'ok': True, 'msg': str(random.randint(0, 1e9))}
- """)
+ return {{'ok': True, 'msg': {code} }}
+ """.format(code=self._get_random_number_code()))
problem = self.build_problem(script=script, cfn="check_func", expect="42")
input_dict = {'1_2_1': '42'}
correct_map = problem.grade_answers(input_dict)
msg = correct_map.get_msg('1_2_1')
- r = random.Random(problem.seed)
- self.assertEqual(msg, str(r.randint(0, 1e9)))
+ self.assertEqual(msg, self._get_random_number_result(problem.seed))
def test_module_imports_inline(self):
'''
@@ -1320,7 +1325,7 @@ class CustomResponseTest(ResponseTest):
class SchematicResponseTest(ResponseTest):
- from response_xml_factory import SchematicResponseXMLFactory
+ from capa.tests.response_xml_factory import SchematicResponseXMLFactory
xml_factory_class = SchematicResponseXMLFactory
def test_grade(self):
@@ -1349,11 +1354,10 @@ class SchematicResponseTest(ResponseTest):
def test_check_function_randomization(self):
# The check function should get a random seed from the problem.
- script = "correct = ['correct' if (submission[0]['num'] == random.randint(0, 1e9)) else 'incorrect']"
+ script = "correct = ['correct' if (submission[0]['num'] == {code}) else 'incorrect']".format(code=self._get_random_number_code())
problem = self.build_problem(answer=script)
- r = random.Random(problem.seed)
- submission_dict = {'num': r.randint(0, 1e9)}
+ submission_dict = {'num': self._get_random_number_result(problem.seed)}
input_dict = {'1_2_1': json.dumps(submission_dict)}
correct_map = problem.grade_answers(input_dict)
@@ -1372,7 +1376,7 @@ class SchematicResponseTest(ResponseTest):
class AnnotationResponseTest(ResponseTest):
- from response_xml_factory import AnnotationResponseXMLFactory
+ from capa.tests.response_xml_factory import AnnotationResponseXMLFactory
xml_factory_class = AnnotationResponseXMLFactory
def test_grade(self):
@@ -1393,7 +1397,7 @@ class AnnotationResponseTest(ResponseTest):
{'correctness': incorrect, 'points': 0, 'answers': {answer_id: 'null'}},
]
- for (index, test) in enumerate(tests):
+ for test in tests:
expected_correctness = test['correctness']
expected_points = test['points']
answers = test['answers']
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index fee80a34ff..a03c0f4160 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -424,7 +424,7 @@ class CapaModule(CapaFields, XModule):
# If we cannot construct the problem HTML,
# then generate an error message instead.
- except Exception, err:
+ except Exception as err:
html = self.handle_problem_html_error(err)
# The convention is to pass the name of the check button
@@ -655,7 +655,7 @@ class CapaModule(CapaFields, XModule):
@staticmethod
def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers")
- get is POST dictionary (Djano QueryDict).
+ get is POST dictionary (Django QueryDict).
The *get* dict has keys of the form 'x_y', which are mapped
to key 'y' in the returned dict. For example,
@@ -739,13 +739,13 @@ class CapaModule(CapaFields, XModule):
# Too late. Cannot submit
if self.closed():
event_info['failure'] = 'closed'
- self.system.track_function('save_problem_check_fail', event_info)
+ self.system.track_function('problem_check_fail', event_info)
raise NotFoundError('Problem is closed')
# Problem submitted. Student should reset before checking again
if self.done and self.rerandomize == "always":
event_info['failure'] = 'unreset'
- self.system.track_function('save_problem_check_fail', event_info)
+ self.system.track_function('problem_check_fail', event_info)
raise NotFoundError('Problem must be reset before it can be checked again')
# Problem queued. Students must wait a specified waittime before they are allowed to submit
@@ -759,6 +759,8 @@ class CapaModule(CapaFields, XModule):
try:
correct_map = self.lcp.grade_answers(answers)
+ self.attempts = self.attempts + 1
+ self.lcp.done = True
self.set_state_from_lcp()
except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
@@ -778,17 +780,13 @@ class CapaModule(CapaFields, XModule):
return {'success': msg}
- except Exception, err:
+ except Exception as err:
if self.system.DEBUG:
msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc()
return {'success': msg}
raise
- self.attempts = self.attempts + 1
- self.lcp.done = True
-
- self.set_state_from_lcp()
self.publish_grade()
# success = correct if ALL questions in this problem are correct
@@ -802,7 +800,7 @@ class CapaModule(CapaFields, XModule):
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
event_info['attempts'] = self.attempts
- self.system.track_function('save_problem_check', event_info)
+ self.system.track_function('problem_check', event_info)
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_state_for_lcp())
@@ -814,12 +812,92 @@ class CapaModule(CapaFields, XModule):
'contents': html,
}
+ def rescore_problem(self):
+ """
+ Checks whether the existing answers to a problem are correct.
+
+ This is called when the correct answer to a problem has been changed,
+ and the grade should be re-evaluated.
+
+ Returns a dict with one key:
+ {'success' : 'correct' | 'incorrect' | AJAX alert msg string }
+
+ Raises NotFoundError if called on a problem that has not yet been
+ answered, or NotImplementedError if it's a problem that cannot be rescored.
+
+ Returns the error messages for exceptions occurring while performing
+ the rescoring, rather than throwing them.
+ """
+ event_info = {'state': self.lcp.get_state(), 'problem_id': self.location.url()}
+
+ if not self.lcp.supports_rescoring():
+ event_info['failure'] = 'unsupported'
+ self.system.track_function('problem_rescore_fail', event_info)
+ raise NotImplementedError("Problem's definition does not support rescoring")
+
+ if not self.done:
+ event_info['failure'] = 'unanswered'
+ self.system.track_function('problem_rescore_fail', event_info)
+ raise NotFoundError('Problem must be answered before it can be graded again')
+
+ # get old score, for comparison:
+ orig_score = self.lcp.get_score()
+ event_info['orig_score'] = orig_score['score']
+ event_info['orig_total'] = orig_score['total']
+
+ try:
+ correct_map = self.lcp.rescore_existing_answers()
+
+ except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
+ log.warning("Input error in capa_module:problem_rescore", exc_info=True)
+ event_info['failure'] = 'input_error'
+ self.system.track_function('problem_rescore_fail', event_info)
+ return {'success': u"Error: {0}".format(inst.message)}
+
+ except Exception as err:
+ event_info['failure'] = 'unexpected'
+ self.system.track_function('problem_rescore_fail', event_info)
+ if self.system.DEBUG:
+ msg = u"Error checking problem: {0}".format(err.message)
+ msg += u'\nTraceback:\n' + traceback.format_exc()
+ return {'success': msg}
+ raise
+
+ # rescoring should have no effect on attempts, so don't
+ # need to increment here, or mark done. Just save.
+ self.set_state_from_lcp()
+
+ self.publish_grade()
+
+ new_score = self.lcp.get_score()
+ event_info['new_score'] = new_score['score']
+ event_info['new_total'] = new_score['total']
+
+ # success = correct if ALL questions in this problem are correct
+ success = 'correct'
+ for answer_id in correct_map:
+ if not correct_map.is_correct(answer_id):
+ success = 'incorrect'
+
+ # NOTE: We are logging both full grading and queued-grading submissions. In the latter,
+ # 'success' will always be incorrect
+ event_info['correct_map'] = correct_map.get_dict()
+ event_info['success'] = success
+ event_info['attempts'] = self.attempts
+ self.system.track_function('problem_rescore', event_info)
+
+ # psychometrics should be called on rescoring requests in the same way as check-problem
+ if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
+ self.system.psychometrics_handler(self.get_state_for_lcp())
+
+ return {'success': success}
+
def save_problem(self, get):
- '''
+ """
Save the passed in answers.
- Returns a dict { 'success' : bool, ['error' : error-msg]},
- with the error key only present if success is False.
- '''
+ Returns a dict { 'success' : bool, 'msg' : message }
+ The message is informative on success, and an error message on failure.
+ """
event_info = dict()
event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url()
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index ac95567946..68285cae0d 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -58,7 +58,7 @@ class CombinedOpenEndedFields(object):
state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state)
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
- scope=Scope.user_state)
+ scope=Scope.user_state)
ready_to_reset = Boolean(
help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state
@@ -66,7 +66,7 @@ class CombinedOpenEndedFields(object):
attempts = Integer(
display_name="Maximum Attempts",
help="The number of times the student can try to answer this problem.", default=1,
- scope=Scope.settings, values = {"min" : 1 }
+ scope=Scope.settings, values={"min" : 1 }
)
is_graded = Boolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = Boolean(
@@ -89,7 +89,7 @@ class CombinedOpenEndedFields(object):
weight = Float(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
- scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
+ scope=Scope.settings, values={"min" : 0 , "step": ".1"}
)
markdown = String(help="Markdown source of this module", scope=Scope.settings)
diff --git a/common/lib/xmodule/xmodule/fields.py b/common/lib/xmodule/xmodule/fields.py
index 8a74856fc1..a9b4be4fcd 100644
--- a/common/lib/xmodule/xmodule/fields.py
+++ b/common/lib/xmodule/xmodule/fields.py
@@ -77,10 +77,8 @@ class Date(ModelType):
else:
return value.isoformat()
-
TIMEDELTA_REGEX = re.compile(r'^((?P\d+?) day(?:s?))?(\s)?((?P\d+?) hour(?:s?))?(\s)?((?P\d+?) minute(?:s)?)?(\s)?((?P\d+?) second(?:s)?)?$')
-
class Timedelta(ModelType):
def from_json(self, time_str):
"""
diff --git a/common/lib/xmodule/xmodule/gst_module.py b/common/lib/xmodule/xmodule/gst_module.py
index 00e8cf1f10..e101d90b4c 100644
--- a/common/lib/xmodule/xmodule/gst_module.py
+++ b/common/lib/xmodule/xmodule/gst_module.py
@@ -84,7 +84,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
xml = html.fromstring(html_string)
- #substitute plot, if presented
+ # substitute plot, if presented
plot_div = ''
plot_el = xml.xpath('//plot')
@@ -95,7 +95,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
element_id=self.html_id,
style=plot_el.get('style', ""))))
- #substitute sliders
+ # substitute sliders
slider_div = '
ErrorLog
+ self._location_errors = {} # location -> ErrorLog
self.metadata_inheritance_cache = None
self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes
@@ -440,7 +440,7 @@ class ModuleStoreBase(ModuleStore):
"""
# check that item is present and raise the promised exceptions if needed
# TODO (vshnayder): post-launch, make errors properties of items
- #self.get_item(location)
+ # self.get_item(location)
errorlog = self._get_errorlog(location)
return errorlog.errors
diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py
index 048aea8867..94823b0be4 100644
--- a/common/lib/xmodule/xmodule/modulestore/draft.py
+++ b/common/lib/xmodule/xmodule/modulestore/draft.py
@@ -15,14 +15,14 @@ def as_draft(location):
"""
Returns the Location that is the draft for `location`
"""
- return Location(location)._replace(revision=DRAFT)
+ return Location(location).replace(revision=DRAFT)
def as_published(location):
"""
Returns the Location that is the published version for `location`
"""
- return Location(location)._replace(revision=None)
+ return Location(location).replace(revision=None)
def wrap_draft(item):
@@ -32,7 +32,7 @@ def wrap_draft(item):
non-draft location in either case
"""
setattr(item, 'is_draft', item.location.revision == DRAFT)
- item.location = item.location._replace(revision=None)
+ item.location = item.location.replace(revision=None)
return item
@@ -234,7 +234,7 @@ class DraftModuleStore(ModuleStoreBase):
# 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)
+ draft_as_non_draft_loc = draft_loc.replace(revision=None)
# does non-draft exist in the collection
# if so, replace it
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 422abbdd73..40288a933b 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -307,7 +307,7 @@ class MongoModuleStore(ModuleStoreBase):
location = Location(result['_id'])
# We need to collate between draft and non-draft
# i.e. draft verticals can have children which are not in non-draft versions
- location = location._replace(revision=None)
+ location = location.replace(revision=None)
location_url = location.url()
if location_url in results_by_url:
existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
index 45e73442d0..01be4c61ab 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/combined_open_ended_modulev1.py
@@ -19,18 +19,18 @@ log = logging.getLogger("mitx.courseware")
# attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 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
-#If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress
-#Metadata overrides this.
+# If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress
+# Metadata overrides this.
IS_SCORED = False
-#If true, then default behavior is to require a file upload or pasted link from a student for this problem.
-#Metadata overrides this.
+# If true, then default behavior is to require a file upload or pasted link from a student for this problem.
+# Metadata overrides this.
ACCEPT_FILE_UPLOAD = False
-#Contains all reasonable bool and case combinations of True
+# Contains all reasonable bool and case combinations of True
TRUE_DICT = ["True", True, "TRUE", "true"]
HUMAN_TASK_TYPE = {
@@ -38,8 +38,8 @@ HUMAN_TASK_TYPE = {
'openended': "edX Assessment",
}
-#Default value that controls whether or not to skip basic spelling checks in the controller
-#Metadata overrides this
+# Default value that controls whether or not to skip basic spelling checks in the controller
+# Metadata overrides this
SKIP_BASIC_CHECKS = False
@@ -74,7 +74,7 @@ class CombinedOpenEndedV1Module():
INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done'
- #Where the templates live for this problem
+ # Where the templates live for this problem
TEMPLATE_DIR = "combinedopenended"
def __init__(self, system, location, definition, descriptor,
@@ -118,21 +118,21 @@ class CombinedOpenEndedV1Module():
self.instance_state = instance_state
self.display_name = instance_state.get('display_name', "Open Ended")
- #We need to set the location here so the child modules can use it
+ # We need to set the location here so the child modules can use it
system.set('location', location)
self.system = system
- #Tells the system which xml definition to load
+ # Tells the system which xml definition to load
self.current_task_number = instance_state.get('current_task_number', 0)
- #This loads the states of the individual children
+ # This loads the states of the individual children
self.task_states = instance_state.get('task_states', [])
- #Overall state of the combined open ended module
+ # Overall state of the combined open ended module
self.state = instance_state.get('state', self.INITIAL)
self.student_attempts = instance_state.get('student_attempts', 0)
self.weight = instance_state.get('weight', 1)
- #Allow reset is true if student has failed the criteria to move to the next child task
+ # Allow reset is true if student has failed the criteria to move to the next child task
self.ready_to_reset = instance_state.get('ready_to_reset', False)
self.attempts = self.instance_state.get('attempts', MAX_ATTEMPTS)
self.is_scored = self.instance_state.get('is_graded', IS_SCORED) in TRUE_DICT
@@ -153,7 +153,7 @@ class CombinedOpenEndedV1Module():
rubric_string = stringify_children(definition['rubric'])
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 = {
'max_score': self._max_score,
'max_attempts': self.attempts,
@@ -243,11 +243,11 @@ class CombinedOpenEndedV1Module():
self.current_task_descriptor = children['descriptors'][current_task_type](self.system)
- #This is the xml object created from the xml definition of the current task
+ # This is the xml object created from the xml definition of the current task
etree_xml = etree.fromstring(self.current_task_xml)
- #This sends the etree_xml object through the descriptor module of the current task, and
- #returns the xml parsed by the descriptor
+ # This sends the etree_xml object through the descriptor module of the current task, and
+ # returns the xml parsed by the descriptor
self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system)
if current_task_state is None and self.current_task_number == 0:
self.current_task = child_task_module(self.system, self.location,
@@ -293,8 +293,9 @@ class CombinedOpenEndedV1Module():
if self.current_task_number > 0:
last_response_data = self.get_last_response(self.current_task_number - 1)
current_response_data = self.get_current_attributes(self.current_task_number)
+
if (current_response_data['min_score_to_attempt'] > last_response_data['score']
- or current_response_data['max_score_to_attempt'] < last_response_data['score']):
+ or current_response_data['max_score_to_attempt'] < last_response_data['score']):
self.state = self.DONE
self.ready_to_reset = True
@@ -307,7 +308,7 @@ class CombinedOpenEndedV1Module():
Output: A dictionary that can be rendered into the combined open ended template.
"""
task_html = self.get_html_base()
- #set context variables and render template
+ # set context variables and render template
context = {
'items': [{'content': task_html}],
@@ -499,7 +500,6 @@ class CombinedOpenEndedV1Module():
"""
changed = self.update_task_states()
if changed:
- #return_html=self.get_html()
pass
return return_html
@@ -730,15 +730,15 @@ class CombinedOpenEndedV1Module():
max_score = None
score = None
if self.is_scored and self.weight is not None:
- #Finds the maximum score of all student attempts and keeps it.
+ # Finds the maximum score of all student attempts and keeps it.
score_mat = []
for i in xrange(0, len(self.task_states)):
- #For each task, extract all student scores on that task (each attempt for each task)
+ # For each task, extract all student scores on that task (each attempt for each task)
last_response = self.get_last_response(i)
max_score = last_response.get('max_score', None)
score = last_response.get('all_scores', None)
if score is not None:
- #Convert none scores and weight scores properly
+ # Convert none scores and weight scores properly
for z in xrange(0, len(score)):
if score[z] is None:
score[z] = 0
@@ -746,19 +746,19 @@ class CombinedOpenEndedV1Module():
score_mat.append(score)
if len(score_mat) > 0:
- #Currently, assume that the final step is the correct one, and that those are the final scores.
- #This will change in the future, which is why the machinery above exists to extract all scores on all steps
- #TODO: better final score handling.
+ # Currently, assume that the final step is the correct one, and that those are the final scores.
+ # This will change in the future, which is why the machinery above exists to extract all scores on all steps
+ # TODO: better final score handling.
scores = score_mat[-1]
score = max(scores)
else:
score = 0
if max_score is not None:
- #Weight the max score if it is not None
+ # Weight the max score if it is not None
max_score *= float(self.weight)
else:
- #Without a max_score, we cannot have a score!
+ # Without a max_score, we cannot have a score!
score = None
score_dict = {
@@ -833,7 +833,7 @@ class CombinedOpenEndedV1Descriptor():
expected_children = ['task', 'rubric', 'prompt']
for child in expected_children:
if len(xml_object.xpath(child)) == 0:
- #This is a staff_facing_error
+ # This is a staff_facing_error
raise ValueError(
"Combined Open Ended definition must include at least one '{0}' tag. Contact the learning sciences group for assistance. {1}".format(
child, xml_object))
@@ -848,6 +848,7 @@ class CombinedOpenEndedV1Descriptor():
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
+
def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended')
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 24af7846d7..1e5b1b233b 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -57,13 +57,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self.queue_name = definition.get('queuename', self.DEFAULT_QUEUE)
self.message_queue_name = definition.get('message-queuename', self.DEFAULT_MESSAGE_QUEUE)
- #This is needed to attach feedback to specific responses later
+ # This is needed to attach feedback to specific responses later
self.submission_id = None
self.grader_id = None
error_message = "No {0} found in problem xml for open ended problem. Contact the learning sciences group for assistance."
if oeparam is None:
- #This is a staff_facing_error
+ # This is a staff_facing_error
raise ValueError(error_message.format('oeparam'))
if self.child_prompt is None:
raise ValueError(error_message.format('prompt'))
@@ -95,14 +95,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
grader_payload = oeparam.find('grader_payload')
grader_payload = grader_payload.text if grader_payload is not None else ''
- #Update grader payload with student id. If grader payload not json, error.
+ # Update grader payload with student id. If grader payload not json, error.
try:
parsed_grader_payload = json.loads(grader_payload)
# NOTE: self.system.location is valid because the capa_module
# __init__ adds it (easiest way to get problem location into
# response types)
except TypeError, ValueError:
- #This is a dev_facing_error
+ # This is a dev_facing_error
log.exception(
"Grader payload from external open ended grading server is not a json object! Object: {0}".format(
grader_payload))
@@ -148,7 +148,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
survey_responses = event_info['survey_responses']
for tag in ['feedback', 'submission_id', 'grader_id', 'score']:
if tag not in survey_responses:
- #This is a student_facing_error
+ # This is a student_facing_error
return {'success': False,
'msg': "Could not find needed tag {0} in the survey responses. Please try submitting again.".format(
tag)}
@@ -158,14 +158,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
feedback = str(survey_responses['feedback'].encode('ascii', 'ignore'))
score = int(survey_responses['score'])
except:
- #This is a dev_facing_error
+ # This is a dev_facing_error
error_message = (
"Could not parse submission id, grader id, "
"or feedback from message_post ajax call. "
"Here is the message data: {0}".format(survey_responses)
)
log.exception(error_message)
- #This is a student_facing_error
+ # This is a student_facing_error
return {'success': False, 'msg': "There was an error saving your feedback. Please contact course staff."}
xqueue = system.get('xqueue')
@@ -201,14 +201,14 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
body=json.dumps(contents)
)
- #Convert error to a success value
+ # Convert error to a success value
success = True
if error:
success = False
self.child_state = self.DONE
- #This is a student_facing_message
+ # This is a student_facing_message
return {'success': success, 'msg': "Successfully submitted your feedback."}
def send_to_grader(self, submission, system):
@@ -249,7 +249,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'submission_time': qtime,
}
- #Update contents with student response and student info
+ # Update contents with student response and student info
contents.update({
'student_info': json.dumps(student_info),
'student_response': submission,
@@ -369,21 +369,21 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
for tag in ['success', 'feedback', 'submission_id', 'grader_id']:
if tag not in response_items:
- #This is a student_facing_error
+ # This is a student_facing_error
return format_feedback('errors', 'Error getting feedback from grader.')
feedback_items = response_items['feedback']
try:
feedback = json.loads(feedback_items)
except (TypeError, ValueError):
- #This is a dev_facing_error
+ # This is a dev_facing_error
log.exception("feedback_items from external open ended grader have invalid json {0}".format(feedback_items))
- #This is a student_facing_error
+ # This is a student_facing_error
return format_feedback('errors', 'Error getting feedback from grader.')
if response_items['success']:
if len(feedback) == 0:
- #This is a student_facing_error
+ # This is a student_facing_error
return format_feedback('errors', 'No feedback available from grader.')
for tag in do_not_render:
@@ -393,7 +393,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
feedback_lst = sorted(feedback.items(), key=get_priority)
feedback_list_part1 = u"\n".join(format_feedback(k, v) for k, v in feedback_lst)
else:
- #This is a student_facing_error
+ # This is a student_facing_error
feedback_list_part1 = format_feedback('errors', response_items['feedback'])
feedback_list_part2 = (u"\n".join([format_feedback_hidden(feedback_type, value)
@@ -470,7 +470,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
try:
score_result = json.loads(score_msg)
except (TypeError, ValueError):
- #This is a dev_facing_error
+ # This is a dev_facing_error
error_message = ("External open ended grader message should be a JSON-serialized dict."
" Received score_msg = {0}".format(score_msg))
log.error(error_message)
@@ -478,7 +478,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return fail
if not isinstance(score_result, dict):
- #This is a dev_facing_error
+ # This is a dev_facing_error
error_message = ("External open ended grader message should be a JSON-serialized dict."
" Received score_result = {0}".format(score_result))
log.error(error_message)
@@ -487,13 +487,13 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
for tag in ['score', 'feedback', 'grader_type', 'success', 'grader_id', 'submission_id']:
if tag not in score_result:
- #This is a dev_facing_error
+ # This is a dev_facing_error
error_message = ("External open ended grader message is missing required tag: {0}"
.format(tag))
log.error(error_message)
fail['feedback'] = error_message
return fail
- #This is to support peer grading
+ # This is to support peer grading
if isinstance(score_result['score'], list):
feedback_items = []
rubric_scores = []
@@ -529,7 +529,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
feedback = feedback_items
score = int(median(score_result['score']))
else:
- #This is for instructor and ML grading
+ # This is for instructor and ML grading
feedback, rubric_score = self._format_feedback(score_result, system)
score = score_result['score']
rubric_scores = [rubric_score]
@@ -608,9 +608,9 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
}
if dispatch not in handlers:
- #This is a dev_facing_error
+ # This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
- #This is a dev_facing_error
+ # This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
before = self.get_progress()
@@ -659,10 +659,10 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
self.send_to_grader(get['student_answer'], system)
self.change_state(self.ASSESSING)
else:
- #Error message already defined
+ # Error message already defined
success = False
else:
- #This is a student_facing_error
+ # This is a student_facing_error
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
return {
@@ -679,7 +679,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
"""
queuekey = get['queuekey']
score_msg = get['xqueue_body']
- #TODO: Remove need for cmap
+ # TODO: Remove need for cmap
self._update_score(score_msg, queuekey, system)
return dict() # No AJAX return is needed
@@ -690,7 +690,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
Input: Modulesystem object
Output: Rendered HTML
"""
- #set context variables and render template
+ # set context variables and render template
eta_string = None
if self.child_state != self.INITIAL:
latest = self.latest_answer()
@@ -749,7 +749,7 @@ class OpenEndedDescriptor():
"""
for child in ['openendedparam']:
if len(xml_object.xpath(child)) != 1:
- #This is a staff_facing_error
+ # This is a staff_facing_error
raise ValueError(
"Open Ended definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
child))
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
index 5c46fbf095..7beca7a72f 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
@@ -54,7 +54,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
@param system: Modulesystem
@return: Rendered HTML
"""
- #set context variables and render template
+ # set context variables and render template
if self.child_state != self.INITIAL:
latest = self.latest_answer()
previous_answer = latest if latest is not None else ''
@@ -93,9 +93,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
}
if dispatch not in handlers:
- #This is a dev_facing_error
+ # This is a dev_facing_error
log.error("Cannot find {0} in handlers in handle_ajax function for open_ended_module.py".format(dispatch))
- #This is a dev_facing_error
+ # This is a dev_facing_error
return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
before = self.get_progress()
@@ -129,7 +129,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
elif self.child_state in (self.POST_ASSESSMENT, self.DONE):
context['read_only'] = True
else:
- #This is a dev_facing_error
+ # This is a dev_facing_error
raise ValueError("Self assessment module is in an illegal state '{0}'".format(self.child_state))
return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
@@ -155,7 +155,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
elif self.child_state == self.DONE:
context['read_only'] = True
else:
- #This is a dev_facing_error
+ # This is a dev_facing_error
raise ValueError("Self Assessment module is in an illegal state '{0}'".format(self.child_state))
return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
@@ -190,10 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
self.new_history_entry(get['student_answer'])
self.change_state(self.ASSESSING)
else:
- #Error message already defined
+ # Error message already defined
success = False
else:
- #This is a student_facing_error
+ # This is a student_facing_error
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
return {
@@ -227,12 +227,12 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
for i in xrange(0, len(score_list)):
score_list[i] = int(score_list[i])
except ValueError:
- #This is a dev_facing_error
+ # This is a dev_facing_error
log.error("Non-integer score value passed to save_assessment ,or no score list present.")
- #This is a student_facing_error
+ # This is a student_facing_error
return {'success': False, 'error': "Error saving your score. Please notify course staff."}
- #Record score as assessment and rubric scores as post assessment
+ # Record score as assessment and rubric scores as post assessment
self.record_latest_score(score)
self.record_latest_post_assessment(json.dumps(score_list))
@@ -272,7 +272,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
try:
rubric_scores = json.loads(latest_post_assessment)
except:
- #This is a dev_facing_error
+ # This is a dev_facing_error
log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment))
rubric_scores = []
return [rubric_scores]
@@ -306,7 +306,7 @@ class SelfAssessmentDescriptor():
expected_children = []
for child in expected_children:
if len(xml_object.xpath(child)) != 1:
- #This is a staff_facing_error
+ # This is a staff_facing_error
raise ValueError(
"Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
child))
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index f0dfca3be6..580f51f6dd 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -62,7 +62,7 @@ class SequenceModule(SequenceFields, XModule):
progress = reduce(Progress.add_counts, progresses)
return progress
- def handle_ajax(self, dispatch, get): # TODO: bounds checking
+ def handle_ajax(self, dispatch, get): # TODO: bounds checking
''' get = request.POST instance '''
if dispatch == 'goto_position':
self.position = int(get['position'])
diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py
index d79d2a163e..9a9666c0b6 100644
--- a/common/lib/xmodule/xmodule/template_module.py
+++ b/common/lib/xmodule/xmodule/template_module.py
@@ -55,7 +55,7 @@ class CustomTagDescriptor(RawDescriptor):
params = dict(xmltree.items())
# cdodge: look up the template as a module
- template_loc = self.location._replace(category='custom_tag_template', name=template_name)
+ template_loc = self.location.replace(category='custom_tag_template', name=template_name)
template_module = modulestore().get_instance(system.course_id, template_loc)
template_module_data = template_module.data
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 7cba4a76b3..deb6f13e20 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -19,6 +19,7 @@ from django.http import QueryDict
from . import test_system
from pytz import UTC
+from capa.correctmap import CorrectMap
class CapaFactory(object):
@@ -597,6 +598,85 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the problem was NOT reset
self.assertTrue('success' in result and not result['success'])
+ def test_rescore_problem_correct(self):
+
+ module = CapaFactory.create(attempts=1, done=True)
+
+ # Simulate that all answers are marked correct, no matter
+ # what the input is, by patching LoncapaResponse.evaluate_answers()
+ with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
+ mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), 'correct')
+ result = module.rescore_problem()
+
+ # Expect that the problem is marked correct
+ self.assertEqual(result['success'], 'correct')
+
+ # Expect that we get no HTML
+ self.assertFalse('contents' in result)
+
+ # Expect that the number of attempts is not incremented
+ self.assertEqual(module.attempts, 1)
+
+ def test_rescore_problem_incorrect(self):
+ # make sure it also works when attempts have been reset,
+ # so add this to the test:
+ module = CapaFactory.create(attempts=0, done=True)
+
+ # Simulate that all answers are marked incorrect, no matter
+ # what the input is, by patching LoncapaResponse.evaluate_answers()
+ with patch('capa.responsetypes.LoncapaResponse.evaluate_answers') as mock_evaluate_answers:
+ mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), 'incorrect')
+ result = module.rescore_problem()
+
+ # Expect that the problem is marked incorrect
+ self.assertEqual(result['success'], 'incorrect')
+
+ # Expect that the number of attempts is not incremented
+ self.assertEqual(module.attempts, 0)
+
+ def test_rescore_problem_not_done(self):
+ # Simulate that the problem is NOT done
+ module = CapaFactory.create(done=False)
+
+ # Try to rescore the problem, and get exception
+ with self.assertRaises(xmodule.exceptions.NotFoundError):
+ module.rescore_problem()
+
+ def test_rescore_problem_not_supported(self):
+ module = CapaFactory.create(done=True)
+
+ # Try to rescore the problem, and get exception
+ with patch('capa.capa_problem.LoncapaProblem.supports_rescoring') as mock_supports_rescoring:
+ mock_supports_rescoring.return_value = False
+ with self.assertRaises(NotImplementedError):
+ module.rescore_problem()
+
+ def _rescore_problem_error_helper(self, exception_class):
+ """Helper to allow testing all errors that rescoring might return."""
+ # Create the module
+ module = CapaFactory.create(attempts=1, done=True)
+
+ # Simulate answering a problem that raises the exception
+ with patch('capa.capa_problem.LoncapaProblem.rescore_existing_answers') as mock_rescore:
+ mock_rescore.side_effect = exception_class(u'test error \u03a9')
+ result = module.rescore_problem()
+
+ # Expect an AJAX alert message in 'success'
+ expected_msg = u'Error: test error \u03a9'
+ self.assertEqual(result['success'], expected_msg)
+
+ # Expect that the number of attempts is NOT incremented
+ self.assertEqual(module.attempts, 1)
+
+ def test_rescore_problem_student_input_error(self):
+ self._rescore_problem_error_helper(StudentInputError)
+
+ def test_rescore_problem_problem_error(self):
+ self._rescore_problem_error_helper(LoncapaProblemError)
+
+ def test_rescore_problem_response_error(self):
+ self._rescore_problem_error_helper(ResponseError)
+
def test_save_problem(self):
module = CapaFactory.create(done=False)
diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py
index e88bf0c588..fed40b690f 100644
--- a/common/lib/xmodule/xmodule/tests/test_conditional.py
+++ b/common/lib/xmodule/xmodule/tests/test_conditional.py
@@ -20,7 +20,7 @@ from . import test_system
class DummySystem(ImportSystem):
- @patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
+ @patch('xmodule.modulestore.xml.OSFS', lambda directory: MemoryFS())
def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
@@ -41,7 +41,8 @@ class DummySystem(ImportSystem):
)
def render_template(self, template, context):
- raise Exception("Shouldn't be called")
+ raise Exception("Shouldn't be called")
+
class ConditionalFactory(object):
"""
@@ -93,7 +94,7 @@ class ConditionalFactory(object):
# return dict:
return {'cond_module': cond_module,
'source_module': source_module,
- 'child_module': child_module }
+ 'child_module': child_module}
class ConditionalModuleBasicTest(unittest.TestCase):
@@ -109,12 +110,11 @@ class ConditionalModuleBasicTest(unittest.TestCase):
'''verify that get_icon_class works independent of condition satisfaction'''
modules = ConditionalFactory.create(self.test_system)
for attempted in ["false", "true"]:
- for icon_class in [ 'other', 'problem', 'video']:
+ for icon_class in ['other', 'problem', 'video']:
modules['source_module'].is_attempted = attempted
modules['child_module'].get_icon_class = lambda: icon_class
self.assertEqual(modules['cond_module'].get_icon_class(), icon_class)
-
def test_get_html(self):
modules = ConditionalFactory.create(self.test_system)
# because test_system returns the repr of the context dict passed to render_template,
@@ -224,4 +224,3 @@ class ConditionalModuleXmlTest(unittest.TestCase):
print "post-attempt ajax: ", ajax
html = ajax['html']
self.assertTrue(any(['This is a secret' in item for item in html]))
-
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 3edc22df43..f5705bf662 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -15,7 +15,7 @@ from xblock.core import XBlock, Scope, String, Integer, Float, ModelType
log = logging.getLogger(__name__)
-def dummy_track(event_type, event):
+def dummy_track(_event_type, _event):
pass
@@ -231,7 +231,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
'''
return self.icon_class
- ### Functions used in the LMS
+ # Functions used in the LMS
def get_score(self):
"""
@@ -272,7 +272,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
'''
return None
- def handle_ajax(self, dispatch, get):
+ def handle_ajax(self, _dispatch, _get):
''' dispatch is last part of the URL.
get is a dictionary-like object '''
return ""
@@ -647,13 +647,13 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# 1. A select editor for fields with a list of possible values (includes Booleans).
# 2. Number editors for integers and floats.
# 3. A generic string editor for anything else (editing JSON representation of the value).
- type = "Generic"
+ editor_type = "Generic"
values = [] if field.values is None else copy.deepcopy(field.values)
if isinstance(values, tuple):
values = list(values)
if isinstance(values, list):
if len(values) > 0:
- type = "Select"
+ editor_type = "Select"
for index, choice in enumerate(values):
json_choice = copy.deepcopy(choice)
if isinstance(json_choice, dict) and 'value' in json_choice:
@@ -662,11 +662,11 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
json_choice = field.to_json(json_choice)
values[index] = json_choice
elif isinstance(field, Integer):
- type = "Integer"
+ editor_type = "Integer"
elif isinstance(field, Float):
- type = "Float"
+ editor_type = "Float"
metadata_fields[field.name] = {'field_name': field.name,
- 'type': type,
+ 'type': editor_type,
'display_name': field.display_name,
'value': field.to_json(value),
'options': values,
@@ -862,7 +862,7 @@ class ModuleSystem(object):
class DoNothingCache(object):
"""A duck-compatible object to use in ModuleSystem when there's no cache."""
- def get(self, key):
+ def get(self, _key):
return None
def set(self, key, value, timeout=None):
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index e1a0e0cf08..33120ec180 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -56,7 +56,6 @@ def get_metadata_from_xml(xml_object, remove=True):
if meta is None:
return ''
dmdata = meta.text
- #log.debug('meta for %s loaded: %s' % (xml_object,dmdata))
if remove:
xml_object.remove(meta)
return dmdata
diff --git a/common/static/coffee/spec/logger_spec.coffee b/common/static/coffee/spec/logger_spec.coffee
index 8fdfb99251..8866daa570 100644
--- a/common/static/coffee/spec/logger_spec.coffee
+++ b/common/static/coffee/spec/logger_spec.coffee
@@ -3,6 +3,11 @@ describe 'Logger', ->
expect(window.log_event).toBe Logger.log
describe 'log', ->
+ it 'sends an event to Segment.io, if the event is whitelisted', ->
+ spyOn(analytics, 'track')
+ Logger.log 'seq_goto', 'data'
+ expect(analytics.track).toHaveBeenCalledWith 'seq_goto', 'data'
+
it 'send a request to log event', ->
spyOn $, 'getWithPrefix'
Logger.log 'example', 'data'
diff --git a/common/static/coffee/src/logger.coffee b/common/static/coffee/src/logger.coffee
index 58395ba831..6da4929fb0 100644
--- a/common/static/coffee/src/logger.coffee
+++ b/common/static/coffee/src/logger.coffee
@@ -1,5 +1,12 @@
class @Logger
+ # events we want sent to Segment.io for tracking
+ SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev"]
+
@log: (event_type, data) ->
+ if event_type in SEGMENT_IO_WHITELIST
+ # Segment.io event tracking
+ analytics.track event_type, data
+
$.getWithPrefix '/event',
event_type: event_type
event: JSON.stringify(data)
diff --git a/common/static/js/vendor/analytics.js b/common/static/js/vendor/analytics.js
new file mode 100644
index 0000000000..a63ff55587
--- /dev/null
+++ b/common/static/js/vendor/analytics.js
@@ -0,0 +1,5538 @@
+;(function(){
+
+/**
+ * Require the given path.
+ *
+ * @param {String} path
+ * @return {Object} exports
+ * @api public
+ */
+
+function require(path, parent, orig) {
+ var resolved = require.resolve(path);
+
+ // lookup failed
+ if (null == resolved) {
+ orig = orig || path;
+ parent = parent || 'root';
+ var err = new Error('Failed to require "' + orig + '" from "' + parent + '"');
+ err.path = orig;
+ err.parent = parent;
+ err.require = true;
+ throw err;
+ }
+
+ var module = require.modules[resolved];
+
+ // perform real require()
+ // by invoking the module's
+ // registered function
+ if (!module.exports) {
+ module.exports = {};
+ module.client = module.component = true;
+ module.call(this, module.exports, require.relative(resolved), module);
+ }
+
+ return module.exports;
+}
+
+/**
+ * Registered modules.
+ */
+
+require.modules = {};
+
+/**
+ * Registered aliases.
+ */
+
+require.aliases = {};
+
+/**
+ * Resolve `path`.
+ *
+ * Lookup:
+ *
+ * - PATH/index.js
+ * - PATH.js
+ * - PATH
+ *
+ * @param {String} path
+ * @return {String} path or null
+ * @api private
+ */
+
+require.resolve = function(path) {
+ if (path.charAt(0) === '/') path = path.slice(1);
+
+ var paths = [
+ path,
+ path + '.js',
+ path + '.json',
+ path + '/index.js',
+ path + '/index.json'
+ ];
+
+ for (var i = 0; i < paths.length; i++) {
+ var path = paths[i];
+ if (require.modules.hasOwnProperty(path)) return path;
+ if (require.aliases.hasOwnProperty(path)) return require.aliases[path];
+ }
+};
+
+/**
+ * Normalize `path` relative to the current path.
+ *
+ * @param {String} curr
+ * @param {String} path
+ * @return {String}
+ * @api private
+ */
+
+require.normalize = function(curr, path) {
+ var segs = [];
+
+ if ('.' != path.charAt(0)) return path;
+
+ curr = curr.split('/');
+ path = path.split('/');
+
+ for (var i = 0; i < path.length; ++i) {
+ if ('..' == path[i]) {
+ curr.pop();
+ } else if ('.' != path[i] && '' != path[i]) {
+ segs.push(path[i]);
+ }
+ }
+
+ return curr.concat(segs).join('/');
+};
+
+/**
+ * Register module at `path` with callback `definition`.
+ *
+ * @param {String} path
+ * @param {Function} definition
+ * @api private
+ */
+
+require.register = function(path, definition) {
+ require.modules[path] = definition;
+};
+
+/**
+ * Alias a module definition.
+ *
+ * @param {String} from
+ * @param {String} to
+ * @api private
+ */
+
+require.alias = function(from, to) {
+ if (!require.modules.hasOwnProperty(from)) {
+ throw new Error('Failed to alias "' + from + '", it does not exist');
+ }
+ require.aliases[to] = from;
+};
+
+/**
+ * Return a require function relative to the `parent` path.
+ *
+ * @param {String} parent
+ * @return {Function}
+ * @api private
+ */
+
+require.relative = function(parent) {
+ var p = require.normalize(parent, '..');
+
+ /**
+ * lastIndexOf helper.
+ */
+
+ function lastIndexOf(arr, obj) {
+ var i = arr.length;
+ while (i--) {
+ if (arr[i] === obj) return i;
+ }
+ return -1;
+ }
+
+ /**
+ * The relative require() itself.
+ */
+
+ function localRequire(path) {
+ var resolved = localRequire.resolve(path);
+ return require(resolved, parent, path);
+ }
+
+ /**
+ * Resolve relative to the parent.
+ */
+
+ localRequire.resolve = function(path) {
+ var c = path.charAt(0);
+ if ('/' == c) return path.slice(1);
+ if ('.' == c) return require.normalize(p, path);
+
+ // resolve deps by returning
+ // the dep in the nearest "deps"
+ // directory
+ var segs = parent.split('/');
+ var i = lastIndexOf(segs, 'deps') + 1;
+ if (!i) i = 0;
+ path = segs.slice(0, i + 1).join('/') + '/deps/' + path;
+ return path;
+ };
+
+ /**
+ * Check if module is defined at `path`.
+ */
+
+ localRequire.exists = function(path) {
+ return require.modules.hasOwnProperty(localRequire.resolve(path));
+ };
+
+ return localRequire;
+};
+require.register("avetisk-defaults/index.js", function(exports, require, module){
+'use strict';
+
+/**
+ * Merge default values.
+ *
+ * @param {Object} dest
+ * @param {Object} defaults
+ * @return {Object}
+ * @api public
+ */
+var defaults = function (dest, src, recursive) {
+ for (var prop in src) {
+ if (recursive && dest[prop] instanceof Object && src[prop] instanceof Object) {
+ dest[prop] = defaults(dest[prop], src[prop], true);
+ } else if (! (prop in dest)) {
+ dest[prop] = src[prop];
+ }
+ }
+
+ return dest;
+};
+
+/**
+ * Expose `defaults`.
+ */
+module.exports = defaults;
+
+});
+require.register("component-clone/index.js", function(exports, require, module){
+
+/**
+ * Module dependencies.
+ */
+
+var type;
+
+try {
+ type = require('type');
+} catch(e){
+ type = require('type-component');
+}
+
+/**
+ * Module exports.
+ */
+
+module.exports = clone;
+
+/**
+ * Clones objects.
+ *
+ * @param {Mixed} any object
+ * @api public
+ */
+
+function clone(obj){
+ switch (type(obj)) {
+ case 'object':
+ var copy = {};
+ for (var key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ copy[key] = clone(obj[key]);
+ }
+ }
+ return copy;
+
+ case 'array':
+ var copy = new Array(obj.length);
+ for (var i = 0, l = obj.length; i < l; i++) {
+ copy[i] = clone(obj[i]);
+ }
+ return copy;
+
+ case 'regexp':
+ // from millermedeiros/amd-utils - MIT
+ var flags = '';
+ flags += obj.multiline ? 'm' : '';
+ flags += obj.global ? 'g' : '';
+ flags += obj.ignoreCase ? 'i' : '';
+ return new RegExp(obj.source, flags);
+
+ case 'date':
+ return new Date(obj.getTime());
+
+ default: // string, number, boolean, …
+ return obj;
+ }
+}
+
+});
+require.register("component-cookie/index.js", function(exports, require, module){
+/**
+ * Encode.
+ */
+
+var encode = encodeURIComponent;
+
+/**
+ * Decode.
+ */
+
+var decode = decodeURIComponent;
+
+/**
+ * Set or get cookie `name` with `value` and `options` object.
+ *
+ * @param {String} name
+ * @param {String} value
+ * @param {Object} options
+ * @return {Mixed}
+ * @api public
+ */
+
+module.exports = function(name, value, options){
+ switch (arguments.length) {
+ case 3:
+ case 2:
+ return set(name, value, options);
+ case 1:
+ return get(name);
+ default:
+ return all();
+ }
+};
+
+/**
+ * Set cookie `name` to `value`.
+ *
+ * @param {String} name
+ * @param {String} value
+ * @param {Object} options
+ * @api private
+ */
+
+function set(name, value, options) {
+ options = options || {};
+ var str = encode(name) + '=' + encode(value);
+
+ if (null == value) options.maxage = -1;
+
+ if (options.maxage) {
+ options.expires = new Date(+new Date + options.maxage);
+ }
+
+ if (options.path) str += '; path=' + options.path;
+ if (options.domain) str += '; domain=' + options.domain;
+ if (options.expires) str += '; expires=' + options.expires.toUTCString();
+ if (options.secure) str += '; secure';
+
+ document.cookie = str;
+}
+
+/**
+ * Return all cookies.
+ *
+ * @return {Object}
+ * @api private
+ */
+
+function all() {
+ return parse(document.cookie);
+}
+
+/**
+ * Get cookie `name`.
+ *
+ * @param {String} name
+ * @return {String}
+ * @api private
+ */
+
+function get(name) {
+ return all()[name];
+}
+
+/**
+ * Parse cookie `str`.
+ *
+ * @param {String} str
+ * @return {Object}
+ * @api private
+ */
+
+function parse(str) {
+ var obj = {};
+ var pairs = str.split(/ *; */);
+ var pair;
+ if ('' == pairs[0]) return obj;
+ for (var i = 0; i < pairs.length; ++i) {
+ pair = pairs[i].split('=');
+ obj[decode(pair[0])] = decode(pair[1]);
+ }
+ return obj;
+}
+
+});
+require.register("component-each/index.js", function(exports, require, module){
+
+/**
+ * Module dependencies.
+ */
+
+var type = require('type');
+
+/**
+ * HOP reference.
+ */
+
+var has = Object.prototype.hasOwnProperty;
+
+/**
+ * Iterate the given `obj` and invoke `fn(val, i)`.
+ *
+ * @param {String|Array|Object} obj
+ * @param {Function} fn
+ * @api public
+ */
+
+module.exports = function(obj, fn){
+ switch (type(obj)) {
+ case 'array':
+ return array(obj, fn);
+ case 'object':
+ if ('number' == typeof obj.length) return array(obj, fn);
+ return object(obj, fn);
+ case 'string':
+ return string(obj, fn);
+ }
+};
+
+/**
+ * Iterate string chars.
+ *
+ * @param {String} obj
+ * @param {Function} fn
+ * @api private
+ */
+
+function string(obj, fn) {
+ for (var i = 0; i < obj.length; ++i) {
+ fn(obj.charAt(i), i);
+ }
+}
+
+/**
+ * Iterate object keys.
+ *
+ * @param {Object} obj
+ * @param {Function} fn
+ * @api private
+ */
+
+function object(obj, fn) {
+ for (var key in obj) {
+ if (has.call(obj, key)) {
+ fn(key, obj[key]);
+ }
+ }
+}
+
+/**
+ * Iterate array-ish.
+ *
+ * @param {Array|Object} obj
+ * @param {Function} fn
+ * @api private
+ */
+
+function array(obj, fn) {
+ for (var i = 0; i < obj.length; ++i) {
+ fn(obj[i], i);
+ }
+}
+});
+require.register("component-event/index.js", function(exports, require, module){
+
+/**
+ * Bind `el` event `type` to `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.bind = function(el, type, fn, capture){
+ if (el.addEventListener) {
+ el.addEventListener(type, fn, capture || false);
+ } else {
+ el.attachEvent('on' + type, fn);
+ }
+ return fn;
+};
+
+/**
+ * Unbind `el` event `type`'s callback `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.unbind = function(el, type, fn, capture){
+ if (el.removeEventListener) {
+ el.removeEventListener(type, fn, capture || false);
+ } else {
+ el.detachEvent('on' + type, fn);
+ }
+ return fn;
+};
+
+});
+require.register("component-inherit/index.js", function(exports, require, module){
+
+module.exports = function(a, b){
+ var fn = function(){};
+ fn.prototype = b.prototype;
+ a.prototype = new fn;
+ a.prototype.constructor = a;
+};
+});
+require.register("component-object/index.js", function(exports, require, module){
+
+/**
+ * HOP ref.
+ */
+
+var has = Object.prototype.hasOwnProperty;
+
+/**
+ * Return own keys in `obj`.
+ *
+ * @param {Object} obj
+ * @return {Array}
+ * @api public
+ */
+
+exports.keys = Object.keys || function(obj){
+ var keys = [];
+ for (var key in obj) {
+ if (has.call(obj, key)) {
+ keys.push(key);
+ }
+ }
+ return keys;
+};
+
+/**
+ * Return own values in `obj`.
+ *
+ * @param {Object} obj
+ * @return {Array}
+ * @api public
+ */
+
+exports.values = function(obj){
+ var vals = [];
+ for (var key in obj) {
+ if (has.call(obj, key)) {
+ vals.push(obj[key]);
+ }
+ }
+ return vals;
+};
+
+/**
+ * Merge `b` into `a`.
+ *
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ * @api public
+ */
+
+exports.merge = function(a, b){
+ for (var key in b) {
+ if (has.call(b, key)) {
+ a[key] = b[key];
+ }
+ }
+ return a;
+};
+
+/**
+ * Return length of `obj`.
+ *
+ * @param {Object} obj
+ * @return {Number}
+ * @api public
+ */
+
+exports.length = function(obj){
+ return exports.keys(obj).length;
+};
+
+/**
+ * Check if `obj` is empty.
+ *
+ * @param {Object} obj
+ * @return {Boolean}
+ * @api public
+ */
+
+exports.isEmpty = function(obj){
+ return 0 == exports.length(obj);
+};
+});
+require.register("component-trim/index.js", function(exports, require, module){
+
+exports = module.exports = trim;
+
+function trim(str){
+ return str.replace(/^\s*|\s*$/g, '');
+}
+
+exports.left = function(str){
+ return str.replace(/^\s*/, '');
+};
+
+exports.right = function(str){
+ return str.replace(/\s*$/, '');
+};
+
+});
+require.register("component-querystring/index.js", function(exports, require, module){
+
+/**
+ * Module dependencies.
+ */
+
+var trim = require('trim');
+
+/**
+ * Parse the given query `str`.
+ *
+ * @param {String} str
+ * @return {Object}
+ * @api public
+ */
+
+exports.parse = function(str){
+ if ('string' != typeof str) return {};
+
+ str = trim(str);
+ if ('' == str) return {};
+
+ var obj = {};
+ var pairs = str.split('&');
+ for (var i = 0; i < pairs.length; i++) {
+ var parts = pairs[i].split('=');
+ obj[parts[0]] = null == parts[1]
+ ? ''
+ : decodeURIComponent(parts[1]);
+ }
+
+ return obj;
+};
+
+/**
+ * Stringify the given `obj`.
+ *
+ * @param {Object} obj
+ * @return {String}
+ * @api public
+ */
+
+exports.stringify = function(obj){
+ if (!obj) return '';
+ var pairs = [];
+ for (var key in obj) {
+ pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
+ }
+ return pairs.join('&');
+};
+
+});
+require.register("component-type/index.js", function(exports, require, module){
+
+/**
+ * toString ref.
+ */
+
+var toString = Object.prototype.toString;
+
+/**
+ * Return the type of `val`.
+ *
+ * @param {Mixed} val
+ * @return {String}
+ * @api public
+ */
+
+module.exports = function(val){
+ switch (toString.call(val)) {
+ case '[object Function]': return 'function';
+ case '[object Date]': return 'date';
+ case '[object RegExp]': return 'regexp';
+ case '[object Arguments]': return 'arguments';
+ case '[object Array]': return 'array';
+ case '[object String]': return 'string';
+ }
+
+ if (val === null) return 'null';
+ if (val === undefined) return 'undefined';
+ if (val && val.nodeType === 1) return 'element';
+ if (val === Object(val)) return 'object';
+
+ return typeof val;
+};
+
+});
+require.register("component-url/index.js", function(exports, require, module){
+
+/**
+ * Parse the given `url`.
+ *
+ * @param {String} str
+ * @return {Object}
+ * @api public
+ */
+
+exports.parse = function(url){
+ var a = document.createElement('a');
+ a.href = url;
+ return {
+ href: a.href,
+ host: a.host || location.host,
+ port: ('0' === a.port || '' === a.port) ? location.port : a.port,
+ hash: a.hash,
+ hostname: a.hostname || location.hostname,
+ pathname: a.pathname.charAt(0) != '/' ? '/' + a.pathname : a.pathname,
+ protocol: !a.protocol || ':' == a.protocol ? location.protocol : a.protocol,
+ search: a.search,
+ query: a.search.slice(1)
+ };
+};
+
+/**
+ * Check if `url` is absolute.
+ *
+ * @param {String} url
+ * @return {Boolean}
+ * @api public
+ */
+
+exports.isAbsolute = function(url){
+ return 0 == url.indexOf('//') || !!~url.indexOf('://');
+};
+
+/**
+ * Check if `url` is relative.
+ *
+ * @param {String} url
+ * @return {Boolean}
+ * @api public
+ */
+
+exports.isRelative = function(url){
+ return !exports.isAbsolute(url);
+};
+
+/**
+ * Check if `url` is cross domain.
+ *
+ * @param {String} url
+ * @return {Boolean}
+ * @api public
+ */
+
+exports.isCrossDomain = function(url){
+ url = exports.parse(url);
+ return url.hostname !== location.hostname
+ || url.port !== location.port
+ || url.protocol !== location.protocol;
+};
+});
+require.register("segmentio-after/index.js", function(exports, require, module){
+
+module.exports = function after (times, func) {
+ // After 0, really?
+ if (times <= 0) return func();
+
+ // That's more like it.
+ return function() {
+ if (--times < 1) {
+ return func.apply(this, arguments);
+ }
+ };
+};
+});
+require.register("segmentio-alias/index.js", function(exports, require, module){
+
+module.exports = function alias (object, aliases) {
+ // For each of our aliases, rename our object's keys.
+ for (var oldKey in aliases) {
+ var newKey = aliases[oldKey];
+ if (object[oldKey] !== undefined) {
+ object[newKey] = object[oldKey];
+ delete object[oldKey];
+ }
+ }
+};
+});
+require.register("component-bind/index.js", function(exports, require, module){
+
+/**
+ * Slice reference.
+ */
+
+var slice = [].slice;
+
+/**
+ * Bind `obj` to `fn`.
+ *
+ * @param {Object} obj
+ * @param {Function|String} fn or string
+ * @return {Function}
+ * @api public
+ */
+
+module.exports = function(obj, fn){
+ if ('string' == typeof fn) fn = obj[fn];
+ if ('function' != typeof fn) throw new Error('bind() requires a function');
+ var args = [].slice.call(arguments, 2);
+ return function(){
+ return fn.apply(obj, args.concat(slice.call(arguments)));
+ }
+};
+
+});
+require.register("segmentio-bind-all/index.js", function(exports, require, module){
+
+var bind = require('bind')
+ , type = require('type');
+
+
+module.exports = function (obj) {
+ for (var key in obj) {
+ var val = obj[key];
+ if (type(val) === 'function') obj[key] = bind(obj, obj[key]);
+ }
+ return obj;
+};
+});
+require.register("segmentio-canonical/index.js", function(exports, require, module){
+module.exports = function canonical () {
+ var tags = document.getElementsByTagName('link');
+ for (var i = 0, tag; tag = tags[i]; i++) {
+ if ('canonical' == tag.getAttribute('rel')) return tag.getAttribute('href');
+ }
+};
+});
+require.register("segmentio-extend/index.js", function(exports, require, module){
+
+module.exports = function extend (object) {
+ // Takes an unlimited number of extenders.
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ // For each extender, copy their properties on our object.
+ for (var i = 0, source; source = args[i]; i++) {
+ if (!source) continue;
+ for (var property in source) {
+ object[property] = source[property];
+ }
+ }
+
+ return object;
+};
+});
+require.register("segmentio-is-email/index.js", function(exports, require, module){
+
+module.exports = function isEmail (string) {
+ return (/.+\@.+\..+/).test(string);
+};
+});
+require.register("segmentio-is-meta/index.js", function(exports, require, module){
+module.exports = function isMeta (e) {
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return true;
+
+ // Logic that handles checks for the middle mouse button, based
+ // on [jQuery](https://github.com/jquery/jquery/blob/master/src/event.js#L466).
+ var which = e.which, button = e.button;
+ if (!which && button !== undefined) {
+ return (!button & 1) && (!button & 2) && (button & 4);
+ } else if (which === 2) {
+ return true;
+ }
+
+ return false;
+};
+});
+require.register("component-json-fallback/index.js", function(exports, require, module){
+/*
+ json2.js
+ 2011-10-19
+
+ Public Domain.
+
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+ See http://www.JSON.org/js.html
+
+
+ This code should be minified before deployment.
+ See http://javascript.crockford.com/jsmin.html
+
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+ NOT CONTROL.
+
+
+ This file creates a global JSON object containing two methods: stringify
+ and parse.
+
+ JSON.stringify(value, replacer, space)
+ value any JavaScript value, usually an object or array.
+
+ replacer an optional parameter that determines how object
+ values are stringified for objects. It can be a
+ function or an array of strings.
+
+ space an optional parameter that specifies the indentation
+ of nested structures. If it is omitted, the text will
+ be packed without extra whitespace. If it is a number,
+ it will specify the number of spaces to indent at each
+ level. If it is a string (such as '\t' or ' '),
+ it contains the characters used to indent at each level.
+
+ This method produces a JSON text from a JavaScript value.
+
+ When an object value is found, if the object contains a toJSON
+ method, its toJSON method will be called and the result will be
+ stringified. A toJSON method does not serialize: it returns the
+ value represented by the name/value pair that should be serialized,
+ or undefined if nothing should be serialized. The toJSON method
+ will be passed the key associated with the value, and this will be
+ bound to the value
+
+ For example, this would serialize Dates as ISO strings.
+
+ Date.prototype.toJSON = function (key) {
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ You can provide an optional replacer method. It will be passed the
+ key and value of each member, with this bound to the containing
+ object. The value that is returned from your method will be
+ serialized. If your method returns undefined, then the member will
+ be excluded from the serialization.
+
+ If the replacer parameter is an array of strings, then it will be
+ used to select the members to be serialized. It filters the results
+ such that only members with keys listed in the replacer array are
+ stringified.
+
+ Values that do not have JSON representations, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays they will be replaced with null. You can use
+ a replacer function to replace those with JSON values.
+ JSON.stringify(undefined) returns undefined.
+
+ The optional space parameter produces a stringification of the
+ value that is filled with line breaks and indentation to make it
+ easier to read.
+
+ If the space parameter is a non-empty string, then that string will
+ be used for indentation. If the space parameter is a number, then
+ the indentation will be that many spaces.
+
+ Example:
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+ text = JSON.stringify([new Date()], function (key, value) {
+ return this[key] instanceof Date ?
+ 'Date(' + this[key] + ')' : value;
+ });
+ // text is '["Date(---current time---)"]'
+
+
+ JSON.parse(text, reviver)
+ This method parses a JSON text to produce an object or array.
+ It can throw a SyntaxError exception.
+
+ The optional reviver parameter is a function that can filter and
+ transform the results. It receives each of the keys and values,
+ and its return value is used instead of the original value.
+ If it returns what it received, then the structure is not modified.
+ If it returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. Values that look like ISO date strings will
+ // be converted to Date objects.
+
+ myData = JSON.parse(text, function (key, value) {
+ var a;
+ if (typeof value === 'string') {
+ a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ if (a) {
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ +a[5], +a[6]));
+ }
+ }
+ return value;
+ });
+
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ var d;
+ if (typeof value === 'string' &&
+ value.slice(0, 5) === 'Date(' &&
+ value.slice(-1) === ')') {
+ d = new Date(value.slice(5, -1));
+ if (d) {
+ return d;
+ }
+ }
+ return value;
+ });
+
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+*/
+
+/*jslint evil: true, regexp: true */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
+ test, toJSON, toString, valueOf
+*/
+
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+var JSON = {};
+
+(function () {
+ 'use strict';
+
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ if (typeof Date.prototype.toJSON !== 'function') {
+
+ Date.prototype.toJSON = function (key) {
+
+ return isFinite(this.valueOf())
+ ? this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z'
+ : null;
+ };
+
+ String.prototype.toJSON =
+ Number.prototype.toJSON =
+ Boolean.prototype.toJSON = function (key) {
+ return this.valueOf();
+ };
+ }
+
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ gap,
+ indent,
+ meta = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ rep;
+
+
+ function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+ escapable.lastIndex = 0;
+ return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
+ var c = meta[a];
+ return typeof c === 'string'
+ ? c
+ : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' : '"' + string + '"';
+ }
+
+
+ function str(key, holder) {
+
+// Produce a string from holder[key].
+
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ length,
+ mind = gap,
+ partial,
+ value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+ if (value && typeof value === 'object' &&
+ typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+ if (typeof rep === 'function') {
+ value = rep.call(holder, key, value);
+ }
+
+// What happens next depends on the value's type.
+
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+ return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+ gap += indent;
+ partial = [];
+
+// Is the value an array?
+
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ length = value.length;
+ for (i = 0; i < length; i += 1) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+ v = partial.length === 0
+ ? '[]'
+ : gap
+ ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
+ : '[' + partial.join(',') + ']';
+ gap = mind;
+ return v;
+ }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+ if (rep && typeof rep === 'object') {
+ length = rep.length;
+ for (i = 0; i < length; i += 1) {
+ if (typeof rep[i] === 'string') {
+ k = rep[i];
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+ v = partial.length === 0
+ ? '{}'
+ : gap
+ ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
+ : '{' + partial.join(',') + '}';
+ gap = mind;
+ return v;
+ }
+ }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+ if (typeof JSON.stringify !== 'function') {
+ JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+ var i;
+ gap = '';
+ indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+ if (typeof space === 'number') {
+ for (i = 0; i < space; i += 1) {
+ indent += ' ';
+ }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+ } else if (typeof space === 'string') {
+ indent = space;
+ }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+ rep = replacer;
+ if (replacer && typeof replacer !== 'function' &&
+ (typeof replacer !== 'object' ||
+ typeof replacer.length !== 'number')) {
+ throw new Error('JSON.stringify');
+ }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+ return str('', {'': value});
+ };
+ }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+ if (typeof JSON.parse !== 'function') {
+ JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+ var j;
+
+ function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+ var k, v, value = holder[key];
+ if (value && typeof value === 'object') {
+ for (k in value) {
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
+ v = walk(value, k);
+ if (v !== undefined) {
+ value[k] = v;
+ } else {
+ delete value[k];
+ }
+ }
+ }
+ }
+ return reviver.call(holder, key, value);
+ }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+ text = String(text);
+ cx.lastIndex = 0;
+ if (cx.test(text)) {
+ text = text.replace(cx, function (a) {
+ return '\\u' +
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/
+ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
+ .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
+ .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+ return typeof reviver === 'function'
+ ? walk({'': j}, '')
+ : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('JSON.parse');
+ };
+ }
+}());
+
+module.exports = JSON
+});
+require.register("segmentio-json/index.js", function(exports, require, module){
+
+module.exports = 'undefined' == typeof JSON
+ ? require('json-fallback')
+ : JSON;
+
+});
+require.register("segmentio-load-date/index.js", function(exports, require, module){
+
+
+/*
+ * Load date.
+ *
+ * For reference: http://www.html5rocks.com/en/tutorials/webperformance/basics/
+ */
+
+var time = new Date()
+ , perf = window.performance;
+
+if (perf && perf.timing && perf.timing.responseEnd) {
+ time = new Date(perf.timing.responseEnd);
+}
+
+module.exports = time;
+});
+require.register("segmentio-load-script/index.js", function(exports, require, module){
+var type = require('type');
+
+
+module.exports = function loadScript (options, callback) {
+ if (!options) throw new Error('Cant load nothing...');
+
+ // Allow for the simplest case, just passing a `src` string.
+ if (type(options) === 'string') options = { src : options };
+
+ var https = document.location.protocol === 'https:';
+
+ // If you use protocol relative URLs, third-party scripts like Google
+ // Analytics break when testing with `file:` so this fixes that.
+ if (options.src && options.src.indexOf('//') === 0) {
+ options.src = https ? 'https:' + options.src : 'http:' + options.src;
+ }
+
+ // Allow them to pass in different URLs depending on the protocol.
+ if (https && options.https) options.src = options.https;
+ else if (!https && options.http) options.src = options.http;
+
+ // Make the `
+
+
-
+%if instructor_tasks is not None:
+ >
+%endif
%block>
<%include file="/courseware/course_navigation.html" args="active_page='instructor'" />
@@ -193,20 +195,78 @@ function goto( mode)
+ %endif
+ %if settings.MITX_FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
+
Course-specific grade adjustment
+
+
+ Specify a particular problem in the course here by its url:
+
+
+
+ You may use just the "urlname" if a problem, or "modulename/urlname" if not.
+ (For example, if the location is i4x://university/course/problem/problemname,
+ then just provide the problemname.
+ If the location is i4x://university/course/notaproblem/someothername, then
+ provide notaproblem/someothername.)
+
+
+ Then select an action:
+
+
+
+
+
These actions run in the background, and status for active tasks will appear in a table below.
+ To see status for all tasks submitted for this problem, click on this button:
+
+
+
+
+
+
%endif
Student-specific grade inspection and adjustment
-
edX email address or their username:
-
-
and, if you want to reset the number of attempts for a problem, the urlname of that problem
- (e.g. if the location is i4x://university/course/problem/problemname, then the urlname is problemname).
-
+
+ Specify the edX email address or username of a student here:
+
+
+
+ Click this, and a link to student's progress page will appear below:
+
+
+
+ Specify a particular problem in the course here by its url:
+
+
+
+ You may use just the "urlname" if a problem, or "modulename/urlname" if not.
+ (For example, if the location is i4x://university/course/problem/problemname,
+ then just provide the problemname.
+ If the location is i4x://university/course/notaproblem/someothername, then
+ provide notaproblem/someothername.)
+
+
+ Then select an action:
+
+ %if settings.MITX_FEATURES.get('ENABLE_COURSE_BACKGROUND_TASKS'):
+
+ %endif
+
%if instructor_access:
-
You may also delete the entire state of a student for a problem:
-
-
To delete the state of other XBlocks specify modulename/urlname, eg
- combinedopenended/Humanities_SA_Peer
+
+ You may also delete the entire state of a student for the specified module:
+
+
Rescoring runs in the background, and status for active tasks will appear in a table below.
+ To see status for all tasks submitted for this course and student, click on this button:
+