Commit d601cee8 by cahrens

Merge branch 'master' into christina/course-settings-drupal

parents 485b5b2d 501830c9
...@@ -75,4 +75,5 @@ Frances Botsford <frances@edx.org> ...@@ -75,4 +75,5 @@ Frances Botsford <frances@edx.org>
Jonah Stanley <Jonah_Stanley@brown.edu> Jonah Stanley <Jonah_Stanley@brown.edu>
Slater Victoroff <slater.r.victoroff@gmail.com> Slater Victoroff <slater.r.victoroff@gmail.com>
Peter Fogg <peter.p.fogg@gmail.com> Peter Fogg <peter.p.fogg@gmail.com>
Renzo Lucioni <renzolucioni@gmail.com> Bethany LaPenta <lapentab@mit.edu>
\ No newline at end of file Renzo Lucioni <renzolucioni@gmail.com>
...@@ -5,6 +5,28 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,28 @@ 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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
XModule: Only write out assets files if the contents have changed.
XModule: Don't delete generated xmodule asset files when compiling (for
instance, when XModule provides a coffeescript file, don't delete
the associated javascript)
Common: Make asset watchers run as singletons (so they won't start if the
watcher is already running in another shell).
Common: Use coffee directly when watching for coffeescript file changes.
Common: Make rake provide better error messages if packages are missing.
Common: Repairs development documentation generation by sphinx.
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 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` value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
......
...@@ -4,3 +4,4 @@ gem 'sass', '3.1.15' ...@@ -4,3 +4,4 @@ gem 'sass', '3.1.15'
gem 'bourbon', '~> 1.3.6' gem 'bourbon', '~> 1.3.6'
gem 'colorize', '~> 0.5.8' gem 'colorize', '~> 0.5.8'
gem 'launchy', '~> 2.1.2' gem 'launchy', '~> 2.1.2'
gem 'sys-proctable', '~> 0.9.3'
...@@ -39,8 +39,6 @@ def get_users_in_course_group_by_role(location, role): ...@@ -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 Create all permission groups for a new course and subscribe the caller into those roles
''' '''
def create_all_course_groups(creator, location): def create_all_course_groups(creator, location):
create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME) create_new_course_group(creator, location, INSTRUCTOR_ROLE_NAME)
create_new_course_group(creator, location, STAFF_ROLE_NAME) create_new_course_group(creator, location, STAFF_ROLE_NAME)
...@@ -57,13 +55,11 @@ def create_new_course_group(creator, location, role): ...@@ -57,13 +55,11 @@ def create_new_course_group(creator, location, role):
return 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): 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 # remove all memberships
instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME)) instructors = Group.objects.get(name=get_course_groupname_for_role(location, INSTRUCTOR_ROLE_NAME))
for user in instructors.user_set.all(): for user in instructors.user_set.all():
...@@ -75,13 +71,11 @@ def _delete_course_group(location): ...@@ -75,13 +71,11 @@ def _delete_course_group(location):
user.groups.remove(staff) user.groups.remove(staff)
user.save() 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): 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)) 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)) new_instructors_group = Group.objects.get(name=get_course_groupname_for_role(dest, INSTRUCTOR_ROLE_NAME))
for user in instructors.user_set.all(): for user in instructors.user_set.all():
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true
from common import type_in_codemirror from common import type_in_codemirror
KEY_CSS = '.key input.policy-key' KEY_CSS = '.key input.policy-key'
...@@ -28,7 +28,14 @@ def i_am_on_advanced_course_settings(step): ...@@ -28,7 +28,14 @@ def i_am_on_advanced_course_settings(step):
@step(u'I press the "([^"]*)" notification button$') @step(u'I press the "([^"]*)" notification button$')
def press_the_notification_button(step, name): def press_the_notification_button(step, name):
css = 'a.%s-button' % name.lower() css = 'a.%s-button' % name.lower()
world.css_click(css)
# Save was clicked if either the save notification bar is gone, or we have a error notification
# overlaying it (expected in the case of typing Object into display_name).
save_clicked = lambda : world.is_css_not_present('.is-shown.wrapper-notification-warning') or \
world.is_css_present('.is-shown.wrapper-notification-error')
assert_true(world.css_click(css, success_condition=save_clicked),
'The save button was not clicked after 5 attempts.')
@step(u'I edit the value of a policy key$') @step(u'I edit the value of a policy key$')
......
#pylint: disable=C0111 # pylint: disable=C0111
#pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_true from nose.tools import assert_true
...@@ -16,7 +16,7 @@ logger = getLogger(__name__) ...@@ -16,7 +16,7 @@ logger = getLogger(__name__)
########### STEP HELPERS ############## ########### STEP HELPERS ##############
@step('I (?:visit|access|open) the Studio homepage$') @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 # To make this go to port 8001, put
# LETTUCE_SERVER_PORT = 8001 # LETTUCE_SERVER_PORT = 8001
# in your settings.py file. # in your settings.py file.
...@@ -26,17 +26,17 @@ def i_visit_the_studio_homepage(step): ...@@ -26,17 +26,17 @@ def i_visit_the_studio_homepage(step):
@step('I am logged into Studio$') @step('I am logged into Studio$')
def i_am_logged_into_studio(step): def i_am_logged_into_studio(_step):
log_into_studio() log_into_studio()
@step('I confirm the alert$') @step('I confirm the alert$')
def i_confirm_with_ok(step): def i_confirm_with_ok(_step):
world.browser.get_alert().accept() world.browser.get_alert().accept()
@step(u'I press the "([^"]*)" delete icon$') @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': if category == 'section':
css = 'a.delete-button.delete-section-button span.delete-icon' css = 'a.delete-button.delete-section-button span.delete-icon'
elif category == 'subsection': elif category == 'subsection':
...@@ -47,7 +47,7 @@ def i_press_the_category_delete_icon(step, category): ...@@ -47,7 +47,7 @@ def i_press_the_category_delete_icon(step, category):
@step('I have opened a new course in Studio$') @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() open_new_course()
...@@ -73,7 +73,6 @@ def create_studio_user( ...@@ -73,7 +73,6 @@ def create_studio_user(
registration.register(studio_user) registration.register(studio_user)
registration.activate() registration.activate()
def fill_in_course_info( def fill_in_course_info(
name='Robot Super Course', name='Robot Super Course',
org='MITx', org='MITx',
...@@ -107,7 +106,7 @@ def log_into_studio( ...@@ -107,7 +106,7 @@ def log_into_studio(
def create_a_course(): def create_a_course():
c = world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course') world.CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
# Add the user to the instructor group of the course # Add the user to the instructor group of the course
# so they will have the permissions to see it in studio # so they will have the permissions to see it in studio
...@@ -147,6 +146,7 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time): ...@@ -147,6 +146,7 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(date_css, desired_date) world.css_fill(date_css, desired_date)
# hit TAB to get to the time field # hit TAB to get to the time field
e = world.css_find(date_css).first e = world.css_find(date_css).first
# pylint: disable=W0212
e._element.send_keys(Keys.TAB) e._element.send_keys(Keys.TAB)
world.css_fill(time_css, desired_time) world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first e = world.css_find(time_css).first
......
#pylint: disable=C0111 # pylint: disable=C0111
#pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
...@@ -8,7 +8,7 @@ from nose.tools import assert_equal ...@@ -8,7 +8,7 @@ from nose.tools import assert_equal
############### ACTIONS #################### ############### ACTIONS ####################
@step('I click the new section link$') @step('I click the New Section link$')
def i_click_new_section_link(_step): def i_click_new_section_link(_step):
link_css = 'a.new-courseware-section-button' link_css = 'a.new-courseware-section-button'
world.css_click(link_css) world.css_click(link_css)
......
#pylint: disable=C0111 # pylint: disable=C0111
#pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from common import * from common import *
......
...@@ -6,13 +6,13 @@ from lettuce import world, step ...@@ -6,13 +6,13 @@ from lettuce import world, step
@step('when I view the video it does not have autoplay enabled') @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')[0]['data-autoplay'] == 'False'
assert world.css_find('.video_control')[0].has_class('play') assert world.css_find('.video_control')[0].has_class('play')
@step('creating a video takes a single click') @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')) assert(not world.is_css_present('.xmodule_VideoModule'))
world.css_click("a[data-location='i4x://edx/templates/video/default']") world.css_click("a[data-location='i4x://edx/templates/video/default']")
assert(world.is_css_present('.xmodule_VideoModule')) assert(world.is_css_present('.xmodule_VideoModule'))
......
...@@ -39,10 +39,7 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links= ...@@ -39,10 +39,7 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
def set_module_info(store, location, post_data): def set_module_info(store, location, post_data):
module = None module = None
try: try:
if location.revision is None: module = store.get_item(location)
module = store.get_item(location)
else:
module = store.get_item(location)
except: except:
pass pass
......
...@@ -99,6 +99,7 @@ class ChecklistTestCase(CourseTestCase): ...@@ -99,6 +99,7 @@ class ChecklistTestCase(CourseTestCase):
'name': self.course.location.name, 'name': self.course.location.name,
'checklist_index': 2}) 'checklist_index': 2})
def get_first_item(checklist): def get_first_item(checklist):
return checklist['items'][0] return checklist['items'][0]
......
...@@ -132,7 +132,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -132,7 +132,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# just pick one vertical # just pick one vertical
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0] 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()})) resp = self.client.get(reverse('edit_unit', kwargs={'location': location.url()}))
self.assertEqual(resp.status_code, 400) self.assertEqual(resp.status_code, 400)
...@@ -224,7 +224,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -224,7 +224,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
draft_store.clone_item(html_module.location, html_module.location) draft_store.clone_item(html_module.location, html_module.location)
html_module = draft_store.get_item(['i4x', 'edX', 'simple', 'html', 'test_html', None]) 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)) self.assertNotIn('graceperiod', own_metadata(html_module))
html_module.lms.graceperiod = new_graceperiod html_module.lms.graceperiod = new_graceperiod
...@@ -369,7 +369,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -369,7 +369,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
''' '''
module_store = modulestore('direct') module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full']) import_from_xml(module_store, 'common/test/data/', ['full'])
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None])) effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.data, '6 hours') self.assertEqual(effort.data, '6 hours')
...@@ -617,12 +616,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -617,12 +616,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None])) items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertEqual(len(items), 0) 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') filesystem = OSFS(root_dir / 'test_export')
self.assertTrue(filesystem.exists(dirname)) self.assertTrue(filesystem.exists(dirname))
query_loc = Location('i4x', location.org, location.course, category_name, None) 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: for item in items:
filesystem = OSFS(root_dir / ('test_export/' + dirname)) filesystem = OSFS(root_dir / ('test_export/' + dirname))
...@@ -768,7 +767,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -768,7 +767,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_prefetch_children(self): def test_prefetch_children(self):
module_store = modulestore('direct') module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full']) import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012') location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
wrapper = MongoCollectionFindWrapper(module_store.collection.find) wrapper = MongoCollectionFindWrapper(module_store.collection.find)
...@@ -864,7 +862,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -864,7 +862,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_create_course_duplicate_course(self): def test_create_course_duplicate_course(self):
"""Test new course creation - error path""" """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) resp = self.client.post(reverse('create_new_course'), self.course_data)
data = parse_json(resp) data = parse_json(resp)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -872,7 +870,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -872,7 +870,7 @@ class ContentStoreTest(ModuleStoreTestCase):
def test_create_course_duplicate_number(self): def test_create_course_duplicate_number(self):
"""Test new course creation - error path""" """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' self.course_data['display_name'] = 'Robot Super Course Two'
resp = self.client.post(reverse('create_new_course'), self.course_data) resp = self.client.post(reverse('create_new_course'), self.course_data)
...@@ -1090,11 +1088,9 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1090,11 +1088,9 @@ class ContentStoreTest(ModuleStoreTestCase):
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
def test_import_metadata_with_attempts_empty_string(self): def test_import_metadata_with_attempts_empty_string(self):
module_store = modulestore('direct') module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['simple']) import_from_xml(module_store, 'common/test/data/', ['simple'])
did_load_item = False did_load_item = False
try: try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None])) module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
......
...@@ -224,14 +224,14 @@ def add_extra_panel_tab(tab_type, course): ...@@ -224,14 +224,14 @@ def add_extra_panel_tab(tab_type, course):
@param course: A course object from the modulestore. @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. @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) course_tabs = copy.copy(course.tabs)
changed = False 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) tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs: 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) course_tabs.append(tab_panel)
changed = True changed = True
return changed, course_tabs return changed, course_tabs
...@@ -244,14 +244,14 @@ def remove_extra_panel_tab(tab_type, course): ...@@ -244,14 +244,14 @@ def remove_extra_panel_tab(tab_type, course):
@param course: A course object from the modulestore. @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. @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) course_tabs = copy.copy(course.tabs)
changed = False 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) tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel in course_tabs: 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] course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True changed = True
return changed, course_tabs return changed, course_tabs
...@@ -12,8 +12,8 @@ from django.core.urlresolvers import reverse ...@@ -12,8 +12,8 @@ from django.core.urlresolvers import reverse
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, \
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError InvalidLocationError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore.course_info_model import get_course_updates, update_course_updates, delete_course_update 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, \ ...@@ -33,9 +33,6 @@ from .component import OPEN_ENDED_COMPONENT_TYPES, \
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles
import datetime import datetime
from django.utils.timezone import UTC from django.utils.timezone import UTC
# TODO: should explicitly enumerate exports with __all__
__all__ = ['course_index', 'create_new_course', 'course_info', __all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings', 'course_info_updates', 'get_course_settings',
'course_config_graders_page', 'course_config_graders_page',
......
...@@ -103,7 +103,7 @@ def clone_item(request): ...@@ -103,7 +103,7 @@ def clone_item(request):
@expect_json @expect_json
def delete_item(request): def delete_item(request):
item_location = request.POST['id'] item_location = request.POST['id']
item_loc = Location(item_location) item_location = Location(item_location)
# check permissions for this user within this course # check permissions for this user within this course
if not has_access(request.user, item_location): if not has_access(request.user, item_location):
...@@ -124,11 +124,11 @@ def delete_item(request): ...@@ -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 # cdodge: we need to remove our parent's pointer to us so that it is no longer dangling
if delete_all_versions: 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: for parent_loc in parent_locs:
parent = modulestore('direct').get_item(parent_loc) parent = modulestore('direct').get_item(parent_loc)
item_url = item_loc.url() item_url = item_location.url()
if item_url in parent.children: if item_url in parent.children:
children = parent.children children = parent.children
children.remove(item_url) children.remove(item_url)
......
...@@ -41,25 +41,25 @@ class CourseDetails(object): ...@@ -41,25 +41,25 @@ class CourseDetails(object):
course.enrollment_start = descriptor.enrollment_start course.enrollment_start = descriptor.enrollment_start
course.enrollment_end = descriptor.enrollment_end course.enrollment_end = descriptor.enrollment_end
temploc = course_location._replace(category='about', name='syllabus') temploc = course_location.replace(category='about', name='syllabus')
try: try:
course.syllabus = get_modulestore(temploc).get_item(temploc).data course.syllabus = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc._replace(name='overview') temploc = temploc.replace(name='overview')
try: try:
course.overview = get_modulestore(temploc).get_item(temploc).data course.overview = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc._replace(name='effort') temploc = temploc.replace(name='effort')
try: try:
course.effort = get_modulestore(temploc).get_item(temploc).data course.effort = get_modulestore(temploc).get_item(temploc).data
except ItemNotFoundError: except ItemNotFoundError:
pass pass
temploc = temploc._replace(name='video') temploc = temploc.replace(name='video')
try: try:
raw_video = get_modulestore(temploc).get_item(temploc).data raw_video = get_modulestore(temploc).get_item(temploc).data
course.intro_video = CourseDetails.parse_video_tag(raw_video) course.intro_video = CourseDetails.parse_video_tag(raw_video)
...@@ -126,16 +126,16 @@ class CourseDetails(object): ...@@ -126,16 +126,16 @@ class CourseDetails(object):
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # 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. # 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']) update_item(temploc, jsondict['syllabus'])
temploc = temploc._replace(name='overview') temploc = temploc.replace(name='overview')
update_item(temploc, jsondict['overview']) update_item(temploc, jsondict['overview'])
temploc = temploc._replace(name='effort') temploc = temploc.replace(name='effort')
update_item(temploc, jsondict['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']) recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video'])
update_item(temploc, recomposed_video_tag) update_item(temploc, recomposed_video_tag)
...@@ -174,10 +174,10 @@ class CourseDetails(object): ...@@ -174,10 +174,10 @@ class CourseDetails(object):
return result 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): class CourseSettingsEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel): if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)):
return obj.__dict__ return obj.__dict__
elif isinstance(obj, Location): elif isinstance(obj, Location):
return obj.dict() return obj.dict()
......
...@@ -235,8 +235,7 @@ PIPELINE_JS = { ...@@ -235,8 +235,7 @@ PIPELINE_JS = {
'source_filenames': sorted( 'source_filenames': sorted(
rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') + rooted_glob(COMMON_ROOT / 'static/', 'coffee/src/**/*.js') +
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js') rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
) + ['js/hesitate.js', 'js/base.js', ) + ['js/hesitate.js', 'js/base.js', 'js/views/feedback.js',
'js/models/feedback.js', 'js/views/feedback.js',
'js/models/section.js', 'js/views/section.js', 'js/models/section.js', 'js/views/section.js',
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js', 'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/views/assets.js'], 'js/views/assets.js'],
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
"js/vendor/jquery.cookie.js", "js/vendor/jquery.cookie.js",
"js/vendor/json2.js", "js/vendor/json2.js",
"js/vendor/underscore-min.js", "js/vendor/underscore-min.js",
"js/vendor/underscore.string.min.js",
"js/vendor/backbone-min.js", "js/vendor/backbone-min.js",
"js/vendor/jquery.leanModal.min.js", "js/vendor/jquery.leanModal.min.js",
"js/vendor/sinon-1.7.1.js", "js/vendor/sinon-1.7.1.js",
......
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")
...@@ -18,79 +18,105 @@ beforeEach -> ...@@ -18,79 +18,105 @@ beforeEach ->
else else
return trimmedText.indexOf(text) != -1; return trimmedText.indexOf(text) != -1;
describe "CMS.Views.Alert as base class", -> describe "CMS.Views.SystemFeedback", ->
beforeEach -> beforeEach ->
@model = new CMS.Models.ConfirmationMessage({ @options =
title: "Portal" title: "Portal"
message: "Welcome to the Aperture Science Computer-Aided Enrichment Center" 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 # 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()
it "renders on initalize", -> @hideSpy = spyOn(CMS.Views.Alert.Confirmation.prototype, 'hide').andCallThrough()
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", -> 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.$(".action-close")).toBeDefined()
expect(view.$('.wrapper')).toBeShown() expect(view.$('.wrapper')).toBeShown()
expect(view.$el).toContainText(@model.get("title")) expect(view.$el).toContainText(@options.title)
expect(view.$el).toContainText(@model.get("message")) expect(view.$el).toContainText(@options.message)
it "close button sends a .hide() message", -> it "close button sends a .hide() message", ->
spyOn(CMS.Views.Alert.prototype, 'hide').andCallThrough() view = new CMS.Views.Alert.Confirmation(@options).show()
view = new CMS.Views.Alert({model: @model})
view.$(".action-close").click() view.$(".action-close").click()
expect(CMS.Views.Alert.prototype.hide).toHaveBeenCalled() expect(@hideSpy).toHaveBeenCalled()
expect(view.$('.wrapper')).toBeHiding() expect(view.$('.wrapper')).toBeHiding()
describe "CMS.Views.Prompt", -> 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 # 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 # just exercises the Prompt rather than asserting on anything. Best I can
# do for now. :( # do for now. :(
it "changes class on body", -> it "changes class on body", ->
# expect($("body")).not.toHaveClass("prompt-is-shown") # 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") # expect($("body")).toHaveClass("prompt-is-shown")
view.hide() view.hide()
# expect($("body")).not.toHaveClass("prompt-is-shown") # expect($("body")).not.toHaveClass("prompt-is-shown")
describe "CMS.Views.Alert click events", -> describe "CMS.Views.SystemFeedback click events", ->
beforeEach -> beforeEach ->
@model = new CMS.Models.WarningMessage( @primaryClickSpy = jasmine.createSpy('primaryClick')
@secondaryClickSpy = jasmine.createSpy('secondaryClick')
@view = new CMS.Views.Notification.Warning(
title: "Unsaved", title: "Unsaved",
message: "Your content is currently Unsaved.", message: "Your content is currently Unsaved.",
actions: actions:
primary: primary:
text: "Save", text: "Save",
class: "save-button", class: "save-button",
click: jasmine.createSpy('primaryClick') click: @primaryClickSpy
secondary: [{ secondary: [{
text: "Revert", text: "Revert",
class: "cancel-button", class: "cancel-button",
click: jasmine.createSpy('secondaryClick') click: @secondaryClickSpy
}] }]
) )
@view.show()
@view = new CMS.Views.Alert({model: @model})
it "should trigger the primary event on a primary click", -> it "should trigger the primary event on a primary click", ->
@view.primaryClick() @view.$(".action-primary").click()
expect(@model.get('actions').primary.click).toHaveBeenCalled() expect(@primaryClickSpy).toHaveBeenCalled()
expect(@secondaryClickSpy).not.toHaveBeenCalled()
it "should trigger the secondary event on a secondary click", -> it "should trigger the secondary event on a secondary click", ->
@view.secondaryClick() @view.$(".action-secondary").click()
expect(@model.get('actions').secondary[0].click).toHaveBeenCalled() expect(@secondaryClickSpy).toHaveBeenCalled()
expect(@primaryClickSpy).not.toHaveBeenCalled()
it "should apply class to primary action", -> it "should apply class to primary action", ->
expect(@view.$(".action-primary")).toHaveClass("save-button") expect(@view.$(".action-primary")).toHaveClass("save-button")
...@@ -100,20 +126,18 @@ describe "CMS.Views.Alert click events", -> ...@@ -100,20 +126,18 @@ describe "CMS.Views.Alert click events", ->
describe "CMS.Views.Notification minShown and maxShown", -> describe "CMS.Views.Notification minShown and maxShown", ->
beforeEach -> beforeEach ->
@model = new CMS.Models.SystemFeedback( @showSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'show')
intent: "saving" @showSpy.andCallThrough()
title: "Saving" @hideSpy = spyOn(CMS.Views.Notification.Saving.prototype, 'hide')
) @hideSpy.andCallThrough()
spyOn(CMS.Views.Notification.prototype, 'show').andCallThrough()
spyOn(CMS.Views.Notification.prototype, 'hide').andCallThrough()
@clock = sinon.useFakeTimers() @clock = sinon.useFakeTimers()
afterEach -> afterEach ->
@clock.restore() @clock.restore()
it "a minShown view should not hide too quickly", -> it "a minShown view should not hide too quickly", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000}) view = new CMS.Views.Notification.Saving({minShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled() view.show()
expect(view.$('.wrapper')).toBeShown() expect(view.$('.wrapper')).toBeShown()
# call hide() on it, but the minShown should prevent it from hiding right away # 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", -> ...@@ -125,8 +149,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding() expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view should hide by itself", -> it "a maxShown view should hide by itself", ->
view = new CMS.Views.Notification({model: @model, maxShown: 1000}) view = new CMS.Views.Notification.Saving({maxShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled() view.show()
expect(view.$('.wrapper')).toBeShown() expect(view.$('.wrapper')).toBeShown()
# wait for the maxShown timeout to expire, and check again # wait for the maxShown timeout to expire, and check again
...@@ -134,13 +158,13 @@ describe "CMS.Views.Notification minShown and maxShown", -> ...@@ -134,13 +158,13 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding() expect(view.$('.wrapper')).toBeHiding()
it "a minShown view can stay visible longer", -> it "a minShown view can stay visible longer", ->
view = new CMS.Views.Notification({model: @model, minShown: 1000}) view = new CMS.Views.Notification.Saving({minShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled() view.show()
expect(view.$('.wrapper')).toBeShown() expect(view.$('.wrapper')).toBeShown()
# wait for the minShown timeout to expire, and check again # wait for the minShown timeout to expire, and check again
@clock.tick(1001) @clock.tick(1001)
expect(CMS.Views.Notification.prototype.hide).not.toHaveBeenCalled() expect(@hideSpy).not.toHaveBeenCalled()
expect(view.$('.wrapper')).toBeShown() expect(view.$('.wrapper')).toBeShown()
# can now hide immediately # can now hide immediately
...@@ -148,8 +172,8 @@ describe "CMS.Views.Notification minShown and maxShown", -> ...@@ -148,8 +172,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding() expect(view.$('.wrapper')).toBeHiding()
it "a maxShown view can hide early", -> it "a maxShown view can hide early", ->
view = new CMS.Views.Notification({model: @model, maxShown: 1000}) view = new CMS.Views.Notification.Saving({maxShown: 1000})
expect(CMS.Views.Notification.prototype.show).toHaveBeenCalled() view.show()
expect(view.$('.wrapper')).toBeShown() expect(view.$('.wrapper')).toBeShown()
# wait 50 milliseconds, and hide it early # wait 50 milliseconds, and hide it early
...@@ -162,7 +186,8 @@ describe "CMS.Views.Notification minShown and maxShown", -> ...@@ -162,7 +186,8 @@ describe "CMS.Views.Notification minShown and maxShown", ->
expect(view.$('.wrapper')).toBeHiding() expect(view.$('.wrapper')).toBeHiding()
it "a view can have both maxShown and minShown", -> 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 # can't hide early
@clock.tick(50) @clock.tick(50)
......
...@@ -18,11 +18,15 @@ $ -> ...@@ -18,11 +18,15 @@ $ ->
$(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) -> $(document).ajaxError (event, jqXHR, ajaxSettings, thrownError) ->
if ajaxSettings.notifyOnError is false if ajaxSettings.notifyOnError is false
return 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") "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 = -> window.onTouchBasedDevice = ->
navigator.userAgent.match /iPhone|iPod|iPad/i navigator.userAgent.match /iPhone|iPod|iPad/i
......
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"
})
});
...@@ -22,22 +22,16 @@ CMS.Models.Section = Backbone.Model.extend({ ...@@ -22,22 +22,16 @@ CMS.Models.Section = Backbone.Model.extend({
}, },
showNotification: function() { showNotification: function() {
if(!this.msg) { if(!this.msg) {
this.msg = new CMS.Models.SystemFeedback({ this.msg = new CMS.Views.Notification.Saving({
intent: "saving", title: gettext("Saving&hellip;"),
title: gettext("Saving&hellip;")
});
}
if(!this.msgView) {
this.msgView = new CMS.Views.Notification({
model: this.msg,
closeIcon: false, closeIcon: false,
minShown: 1250 minShown: 1250
}); });
} }
this.msgView.show(); this.msg.show();
}, },
hideNotification: function() { hideNotification: function() {
if(!this.msgView) { return; } if(!this.msg) { return; }
this.msgView.hide(); this.msg.hide();
} }
}); });
...@@ -9,7 +9,7 @@ function removeAsset(e){ ...@@ -9,7 +9,7 @@ function removeAsset(e){
e.preventDefault(); e.preventDefault();
var that = this; var that = this;
var msg = new CMS.Models.ConfirmAssetDeleteMessage({ var msg = new CMS.Views.Prompt.Confirmation({
title: gettext("Delete File Confirmation"), title: gettext("Delete File Confirmation"),
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"), message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
actions: { actions: {
...@@ -17,15 +17,17 @@ function removeAsset(e){ ...@@ -17,15 +17,17 @@ function removeAsset(e){
text: gettext("OK"), text: gettext("OK"),
click: function(view) { click: function(view) {
// call the back-end to actually remove the asset // call the back-end to actually remove the asset
$.post(view.model.get('remove_asset_url'), var url = $('.asset-library').data('remove-asset-callback-url');
{ 'location': view.model.get('asset_location') }, var row = $(that).closest('tr');
$.post(url,
{ 'location': row.data('id') },
function() { function() {
// show the post-commit confirmation // show the post-commit confirmation
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false'); $(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
view.model.get('row_to_remove').remove(); row.remove();
analytics.track('Deleted Asset', { analytics.track('Deleted Asset', {
'course': course_location_analytics, 'course': course_location_analytics,
'id': view.model.get('asset_location') 'id': row.data('id')
}); });
} }
); );
...@@ -38,24 +40,9 @@ function removeAsset(e){ ...@@ -38,24 +40,9 @@ function removeAsset(e){
view.hide(); view.hide();
} }
}] }]
}, }
remove_asset_url: $('.asset-library').data('remove-asset-callback-url'),
asset_location: $(this).closest('tr').data('id'),
row_to_remove: $(this).closest('tr')
}); });
return msg.show();
// workaround for now. We can't spawn multiple instances of the Prompt View
// so for now, a bit of hackery to just make sure we have a single instance
// note: confirm_delete_prompt is in asset_index.html
if (confirm_delete_prompt === null)
confirm_delete_prompt = new CMS.Views.Prompt({model: msg});
else
{
confirm_delete_prompt.model = msg;
confirm_delete_prompt.show();
}
return;
} }
function showUploadModal(e) { function showUploadModal(e) {
...@@ -125,4 +112,4 @@ function displayFinishedUpload(xhr) { ...@@ -125,4 +112,4 @@ function displayFinishedUpload(xhr) {
'course': course_location_analytics, 'course': course_location_analytics,
'asset_url': resp.url 'asset_url': resp.url
}); });
} }
\ No newline at end of file
CMS.Views.Alert = Backbone.View.extend({ CMS.Views.SystemFeedback = Backbone.View.extend({
options: { 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? shown: true, // is this view currently being shown?
icon: true, // should we render an icon related to the message intent? 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? 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) 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) 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() { 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(); var tpl = $("#system-feedback-tpl").text();
if(!tpl) { if(!tpl) {
console.error("Couldn't load system-feedback template"); console.error("Couldn't load system-feedback template");
} }
this.template = _.template(tpl); this.template = _.template(tpl);
this.setElement($("#page-"+this.options.type)); 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; return this;
}, },
events: { // public API: show() and hide()
"click .action-close": "hide",
"click .action-primary": "primaryClick",
"click .action-secondary": "secondaryClick"
},
show: function() { show: function() {
clearTimeout(this.hideTimeout); clearTimeout(this.hideTimeout);
this.options.shown = true; this.options.shown = true;
this.shownAt = new Date(); this.shownAt = new Date();
this.render(); this.render();
if($.isNumeric(this.options.maxShown)) { if($.isNumeric(this.options.maxShown)) {
this.hideTimeout = setTimeout($.proxy(this.hide, this), this.hideTimeout = setTimeout(_.bind(this.hide, this),
this.options.maxShown); this.options.maxShown);
} }
return this; return this;
...@@ -43,7 +68,7 @@ CMS.Views.Alert = Backbone.View.extend({ ...@@ -43,7 +68,7 @@ CMS.Views.Alert = Backbone.View.extend({
this.options.minShown > new Date() - this.shownAt) this.options.minShown > new Date() - this.shownAt)
{ {
clearTimeout(this.hideTimeout); clearTimeout(this.hideTimeout);
this.hideTimeout = setTimeout($.proxy(this.hide, this), this.hideTimeout = setTimeout(_.bind(this.hide, this),
this.options.minShown - (new Date() - this.shownAt)); this.options.minShown - (new Date() - this.shownAt));
} else { } else {
this.options.shown = false; this.options.shown = false;
...@@ -52,40 +77,64 @@ CMS.Views.Alert = Backbone.View.extend({ ...@@ -52,40 +77,64 @@ CMS.Views.Alert = Backbone.View.extend({
} }
return this; return this;
}, },
primaryClick: function() { // the rest of the API should be considered semi-private
var actions = this.model.get("actions"); 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();
parent.active.undelegateEvents();
}
this.$el.html(this.template(this.options));
parent.active = this;
return this;
},
primaryClick: function(event) {
var actions = this.options.actions;
if(!actions) { return; } if(!actions) { return; }
var primary = actions.primary; var primary = actions.primary;
if(!primary) { return; } if(!primary) { return; }
if(primary.click) { if(primary.click) {
primary.click.call(this.model, this); primary.click.call(event.target, this, event);
} }
}, },
secondaryClick: function(e) { secondaryClick: function(event) {
var actions = this.model.get("actions"); var actions = this.options.actions;
if(!actions) { return; } if(!actions) { return; }
var secondaryList = actions.secondary; var secondaryList = actions.secondary;
if(!secondaryList) { return; } if(!secondaryList) { return; }
// which secondary action was clicked? // which secondary action was clicked?
var i = 0; // default to the first secondary action (easier for testing) var i = 0; // default to the first secondary action (easier for testing)
if(e && e.target) { if(event && event.target) {
i = _.indexOf(this.$(".action-secondary"), e.target); i = _.indexOf(this.$(".action-secondary"), event.target);
} }
var secondary = this.model.get("actions").secondary[i]; var secondary = secondaryList[i];
if(secondary.click) { if(secondary.click) {
secondary.click.call(this.model, this); secondary.click.call(event.target, this, event);
} }
} }
}); });
CMS.Views.Notification = CMS.Views.Alert.extend({ CMS.Views.Alert = CMS.Views.SystemFeedback.extend({
options: $.extend({}, CMS.Views.Alert.prototype.options, { 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", type: "notification",
closeIcon: false closeIcon: false
}) })
}); });
CMS.Views.Prompt = CMS.Views.Alert.extend({ CMS.Views.Prompt = CMS.Views.SystemFeedback.extend({
options: $.extend({}, CMS.Views.Alert.prototype.options, { options: $.extend({}, CMS.Views.SystemFeedback.prototype.options, {
type: "prompt", type: "prompt",
closeIcon: false, closeIcon: false,
icon: false icon: false
...@@ -98,6 +147,27 @@ CMS.Views.Prompt = CMS.Views.Alert.extend({ ...@@ -98,6 +147,27 @@ CMS.Views.Prompt = CMS.Views.Alert.extend({
$body.removeClass('prompt-is-shown'); $body.removeClass('prompt-is-shown');
} }
// super() in Javascript has awkward syntax :( // 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;
});
});
...@@ -67,7 +67,7 @@ CMS.Views.SectionEdit = Backbone.View.extend({ ...@@ -67,7 +67,7 @@ CMS.Views.SectionEdit = Backbone.View.extend({
showInvalidMessage: function(model, error, options) { showInvalidMessage: function(model, error, options) {
model.set("name", model.previous("name")); model.set("name", model.previous("name"));
var that = this; var that = this;
var msg = new CMS.Models.ErrorMessage({ var prompt = new CMS.Views.Prompt.Error({
title: gettext("Your change could not be saved"), title: gettext("Your change could not be saved"),
message: error, message: error,
actions: { actions: {
...@@ -80,6 +80,6 @@ CMS.Views.SectionEdit = Backbone.View.extend({ ...@@ -80,6 +80,6 @@ CMS.Views.SectionEdit = Backbone.View.extend({
} }
} }
}); });
new CMS.Views.Prompt({model: msg}); prompt.show();
} }
}); });
...@@ -8,12 +8,6 @@ ...@@ -8,12 +8,6 @@
<%block name="jsextra"> <%block name="jsextra">
<script src="${static.url('js/vendor/mustache.js')}"></script> <script src="${static.url('js/vendor/mustache.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/assets.js')}"></script>
<script type='text/javascript'>
// we just want a singleton
confirm_delete_prompt = null;
</script>
</%block> </%block>
<%block name="content"> <%block name="content">
...@@ -99,7 +93,7 @@ ...@@ -99,7 +93,7 @@
</td> </td>
<td class="delete-col"> <td class="delete-col">
<a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a> <a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a>
</td> </td>
</tr> </tr>
% endfor % endfor
</tbody> </tbody>
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
<script type="text/javascript" src="/jsi18n/"></script> <script type="text/javascript" src="/jsi18n/"></script>
<script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/json2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/markitup/jquery.markitup.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/markitup/sets/wiki/set.js')}"></script>
...@@ -54,7 +55,6 @@ ...@@ -54,7 +55,6 @@
<script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script> <script type="text/javascript" src="${static.url('js/vendor/CodeMirror/css.js')}"></script>
<script type="text/javascript" src="//www.youtube.com/player_api"></script> <script type="text/javascript" src="//www.youtube.com/player_api"></script>
<script src="${static.url('js/models/feedback.js')}"></script>
<script src="${static.url('js/views/feedback.js')}"></script> <script src="${static.url('js/views/feedback.js')}"></script>
<!-- view --> <!-- view -->
......
#pylint: disable=C0111 # pylint: disable=C0111
#pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from .factories import * from .factories import *
......
...@@ -58,10 +58,16 @@ def css_find(css, wait_time=5): ...@@ -58,10 +58,16 @@ def css_find(css, wait_time=5):
@world.absorb @world.absorb
def css_click(css_selector, index=0, attempts=5): def css_click(css_selector, index=0, attempts=5, success_condition=lambda:True):
""" """
Perform a click on a CSS selector, retrying if it initially fails Perform a click on a CSS selector, retrying if it initially fails.
This function will return if the click worked (since it is try/excepting all errors)
This function handles errors that may be thrown if the component cannot be clicked on.
However, there are cases where an error may not be thrown, and yet the operation did not
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the click worked.
This function will return True if the click worked (taking into account both errors and the optional
success_condition).
""" """
assert is_css_present(css_selector) assert is_css_present(css_selector)
attempt = 0 attempt = 0
...@@ -69,8 +75,9 @@ def css_click(css_selector, index=0, attempts=5): ...@@ -69,8 +75,9 @@ def css_click(css_selector, index=0, attempts=5):
while attempt < attempts: while attempt < attempts:
try: try:
world.css_find(css_selector)[index].click() world.css_find(css_selector)[index].click()
result = True if success_condition():
break result = True
break
except WebDriverException: except WebDriverException:
# Occasionally, MathJax or other JavaScript can cover up # Occasionally, MathJax or other JavaScript can cover up
# an element temporarily. # an element temporarily.
......
import json import json
import logging import logging
import os
import pytz import pytz
import datetime import datetime
import dateutil.parser import dateutil.parser
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponse from django.http import HttpResponse
from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf import settings from django.conf import settings
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
...@@ -22,6 +20,7 @@ LOGFIELDS = ['username', 'ip', 'event_source', 'event_type', 'event', 'agent', ' ...@@ -22,6 +20,7 @@ LOGFIELDS = ['username', 'ip', 'event_source', 'event_type', 'event', 'agent', '
def log_event(event): def log_event(event):
"""Write tracking event to log file, and optionally to TrackingLog model."""
event_str = json.dumps(event) event_str = json.dumps(event)
log.info(event_str[:settings.TRACK_MAX_EVENT]) log.info(event_str[:settings.TRACK_MAX_EVENT])
if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'): if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
...@@ -34,6 +33,11 @@ def log_event(event): ...@@ -34,6 +33,11 @@ def log_event(event):
def user_track(request): 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 try: # TODO: Do the same for many of the optional META parameters
username = request.user.username username = request.user.username
except: except:
...@@ -50,7 +54,6 @@ def user_track(request): ...@@ -50,7 +54,6 @@ def user_track(request):
except: except:
agent = '' agent = ''
# TODO: Move a bunch of this into log_event
event = { event = {
"username": username, "username": username,
"session": scookie, "session": scookie,
...@@ -68,6 +71,7 @@ def user_track(request): ...@@ -68,6 +71,7 @@ def user_track(request):
def server_track(request, event_type, event, page=None): def server_track(request, event_type, event, page=None):
"""Log events related to server requests."""
try: try:
username = request.user.username username = request.user.username
except: except:
...@@ -95,9 +99,52 @@ def server_track(request, event_type, event, page=None): ...@@ -95,9 +99,52 @@ def server_track(request, event_type, event, page=None):
log_event(event) 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 @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def view_tracking_log(request, args=''): def view_tracking_log(request, args=''):
"""View to output contents of TrackingLog model. For staff use only."""
if not request.user.is_staff: if not request.user.is_staff:
return redirect('/') return redirect('/')
nlen = 100 nlen = 100
......
import re
import json import json
import logging import logging
import static_replace import static_replace
......
...@@ -12,8 +12,8 @@ from path import path ...@@ -12,8 +12,8 @@ from path import path
from cStringIO import StringIO from cStringIO import StringIO
from collections import defaultdict from collections import defaultdict
from .calc import UndefinedVariable from calc import UndefinedVariable
from .capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
logging.basicConfig(format="%(levelname)s %(message)s") logging.basicConfig(format="%(levelname)s %(message)s")
......
"""
Tests to verify that CorrectMap behaves correctly
"""
import unittest import unittest
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
import datetime import datetime
class CorrectMapTest(unittest.TestCase): class CorrectMapTest(unittest.TestCase):
"""
Tests to verify that CorrectMap behaves correctly
"""
def setUp(self): def setUp(self):
self.cmap = CorrectMap() self.cmap = CorrectMap()
def test_set_input_properties(self): def test_set_input_properties(self):
# Set the correctmap properties for two inputs # Set the correctmap properties for two inputs
self.cmap.set(answer_id='1_2_1', self.cmap.set(
correctness='correct', answer_id='1_2_1',
npoints=5, correctness='correct',
msg='Test message', npoints=5,
hint='Test hint', msg='Test message',
hintmode='always', hint='Test hint',
queuestate={'key':'secretstring', hintmode='always',
'time':'20130228100026'}) queuestate={
'key': 'secretstring',
self.cmap.set(answer_id='2_2_1', 'time': '20130228100026'
correctness='incorrect', }
npoints=None, )
msg=None,
hint=None, self.cmap.set(
hintmode=None, answer_id='2_2_1',
queuestate=None) correctness='incorrect',
npoints=None,
msg=None,
hint=None,
hintmode=None,
queuestate=None
)
# Assert that each input has the expected properties # Assert that each input has the expected properties
self.assertTrue(self.cmap.is_correct('1_2_1')) self.assertTrue(self.cmap.is_correct('1_2_1'))
...@@ -62,7 +75,6 @@ class CorrectMapTest(unittest.TestCase): ...@@ -62,7 +75,6 @@ class CorrectMapTest(unittest.TestCase):
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', '')) self.assertFalse(self.cmap.is_right_queuekey('2_2_1', ''))
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None)) self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None))
def test_get_npoints(self): def test_get_npoints(self):
# Set the correctmap properties for 4 inputs # Set the correctmap properties for 4 inputs
# 1) correct, 5 points # 1) correct, 5 points
...@@ -70,25 +82,35 @@ class CorrectMapTest(unittest.TestCase): ...@@ -70,25 +82,35 @@ class CorrectMapTest(unittest.TestCase):
# 3) incorrect, 5 points # 3) incorrect, 5 points
# 4) incorrect, None points # 4) incorrect, None points
# 5) correct, 0 points # 5) correct, 0 points
self.cmap.set(answer_id='1_2_1', self.cmap.set(
correctness='correct', answer_id='1_2_1',
npoints=5) correctness='correct',
npoints=5
self.cmap.set(answer_id='2_2_1', )
correctness='correct',
npoints=None) self.cmap.set(
answer_id='2_2_1',
self.cmap.set(answer_id='3_2_1', correctness='correct',
correctness='incorrect', npoints=None
npoints=5) )
self.cmap.set(answer_id='4_2_1', self.cmap.set(
correctness='incorrect', answer_id='3_2_1',
npoints=None) correctness='incorrect',
npoints=5
self.cmap.set(answer_id='5_2_1', )
correctness='correct',
npoints=0) self.cmap.set(
answer_id='4_2_1',
correctness='incorrect',
npoints=None
)
self.cmap.set(
answer_id='5_2_1',
correctness='correct',
npoints=0
)
# Assert that we get the expected points # Assert that we get the expected points
# If points assigned --> npoints # If points assigned --> npoints
...@@ -100,7 +122,6 @@ class CorrectMapTest(unittest.TestCase): ...@@ -100,7 +122,6 @@ class CorrectMapTest(unittest.TestCase):
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0) self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
def test_set_overall_message(self): def test_set_overall_message(self):
# Default is an empty string string # Default is an empty string string
...@@ -118,14 +139,18 @@ class CorrectMapTest(unittest.TestCase): ...@@ -118,14 +139,18 @@ class CorrectMapTest(unittest.TestCase):
def test_update_from_correctmap(self): def test_update_from_correctmap(self):
# Initialize a CorrectMap with some properties # Initialize a CorrectMap with some properties
self.cmap.set(answer_id='1_2_1', self.cmap.set(
correctness='correct', answer_id='1_2_1',
npoints=5, correctness='correct',
msg='Test message', npoints=5,
hint='Test hint', msg='Test message',
hintmode='always', hint='Test hint',
queuestate={'key':'secretstring', hintmode='always',
'time':'20130228100026'}) queuestate={
'key': 'secretstring',
'time': '20130228100026'
}
)
self.cmap.set_overall_message("Test message") self.cmap.set_overall_message("Test message")
...@@ -133,14 +158,17 @@ class CorrectMapTest(unittest.TestCase): ...@@ -133,14 +158,17 @@ class CorrectMapTest(unittest.TestCase):
# as the first cmap # as the first cmap
other_cmap = CorrectMap() other_cmap = CorrectMap()
other_cmap.update(self.cmap) other_cmap.update(self.cmap)
# Assert that it has all the same properties
self.assertEqual(other_cmap.get_overall_message(),
self.cmap.get_overall_message())
self.assertEqual(other_cmap.get_dict(),
self.cmap.get_dict())
# Assert that it has all the same properties
self.assertEqual(
other_cmap.get_overall_message(),
self.cmap.get_overall_message()
)
self.assertEqual(
other_cmap.get_dict(),
self.cmap.get_dict()
)
def test_update_from_invalid(self): def test_update_from_invalid(self):
# Should get an exception if we try to update() a CorrectMap # Should get an exception if we try to update() a CorrectMap
......
...@@ -279,7 +279,7 @@ class CapaModule(CapaFields, XModule): ...@@ -279,7 +279,7 @@ class CapaModule(CapaFields, XModule):
""" """
Return True/False to indicate whether to show the "Check" button. Return True/False to indicate whether to show the "Check" button.
""" """
submitted_without_reset = (self.is_completed() and self.rerandomize == "always") submitted_without_reset = (self.is_submitted() and self.rerandomize == "always")
# If the problem is closed (past due / too many attempts) # If the problem is closed (past due / too many attempts)
# then we do NOT show the "check" button # then we do NOT show the "check" button
...@@ -302,7 +302,7 @@ class CapaModule(CapaFields, XModule): ...@@ -302,7 +302,7 @@ class CapaModule(CapaFields, XModule):
# then do NOT show the reset button. # then do NOT show the reset button.
# If the problem hasn't been submitted yet, then do NOT show # If the problem hasn't been submitted yet, then do NOT show
# the reset button. # the reset button.
if (self.closed() and not is_survey_question) or not self.is_completed(): if (self.closed() and not is_survey_question) or not self.is_submitted():
return False return False
else: else:
return True return True
...@@ -322,7 +322,7 @@ class CapaModule(CapaFields, XModule): ...@@ -322,7 +322,7 @@ class CapaModule(CapaFields, XModule):
return not self.closed() return not self.closed()
else: else:
is_survey_question = (self.max_attempts == 0) is_survey_question = (self.max_attempts == 0)
needs_reset = self.is_completed() and self.rerandomize == "always" needs_reset = self.is_submitted() and self.rerandomize == "always"
# If the student has unlimited attempts, and their answers # If the student has unlimited attempts, and their answers
# are not randomized, then we do not need a save button # are not randomized, then we do not need a save button
...@@ -424,7 +424,7 @@ class CapaModule(CapaFields, XModule): ...@@ -424,7 +424,7 @@ class CapaModule(CapaFields, XModule):
# If we cannot construct the problem HTML, # If we cannot construct the problem HTML,
# then generate an error message instead. # then generate an error message instead.
except Exception, err: except Exception as err:
html = self.handle_problem_html_error(err) html = self.handle_problem_html_error(err)
# The convention is to pass the name of the check button # The convention is to pass the name of the check button
...@@ -516,13 +516,18 @@ class CapaModule(CapaFields, XModule): ...@@ -516,13 +516,18 @@ class CapaModule(CapaFields, XModule):
return False return False
def is_completed(self): def is_submitted(self):
# used by conditional module """
# return self.answer_available() Used to decide to show or hide RESET or CHECK buttons.
Means that student submitted problem and nothing more.
Problem can be completely wrong.
Pressing RESET button makes this function to return False.
"""
return self.lcp.done return self.lcp.done
def is_attempted(self): def is_attempted(self):
# used by conditional module """Used by conditional module"""
return self.attempts > 0 return self.attempts > 0
def is_correct(self): def is_correct(self):
...@@ -655,7 +660,7 @@ class CapaModule(CapaFields, XModule): ...@@ -655,7 +660,7 @@ class CapaModule(CapaFields, XModule):
@staticmethod @staticmethod
def make_dict_of_responses(get): def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers") '''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 The *get* dict has keys of the form 'x_y', which are mapped
to key 'y' in the returned dict. For example, to key 'y' in the returned dict. For example,
...@@ -739,13 +744,13 @@ class CapaModule(CapaFields, XModule): ...@@ -739,13 +744,13 @@ class CapaModule(CapaFields, XModule):
# Too late. Cannot submit # Too late. Cannot submit
if self.closed(): if self.closed():
event_info['failure'] = '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') raise NotFoundError('Problem is closed')
# Problem submitted. Student should reset before checking again # Problem submitted. Student should reset before checking again
if self.done and self.rerandomize == "always": if self.done and self.rerandomize == "always":
event_info['failure'] = 'unreset' 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') 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 # Problem queued. Students must wait a specified waittime before they are allowed to submit
...@@ -759,6 +764,8 @@ class CapaModule(CapaFields, XModule): ...@@ -759,6 +764,8 @@ class CapaModule(CapaFields, XModule):
try: try:
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
self.attempts = self.attempts + 1
self.lcp.done = True
self.set_state_from_lcp() self.set_state_from_lcp()
except (StudentInputError, ResponseError, LoncapaProblemError) as inst: except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
...@@ -778,17 +785,13 @@ class CapaModule(CapaFields, XModule): ...@@ -778,17 +785,13 @@ class CapaModule(CapaFields, XModule):
return {'success': msg} return {'success': msg}
except Exception, err: except Exception as err:
if self.system.DEBUG: if self.system.DEBUG:
msg = "Error checking problem: " + str(err) msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc() msg += '\nTraceback:\n' + traceback.format_exc()
return {'success': msg} return {'success': msg}
raise raise
self.attempts = self.attempts + 1
self.lcp.done = True
self.set_state_from_lcp()
self.publish_grade() self.publish_grade()
# success = correct if ALL questions in this problem are correct # success = correct if ALL questions in this problem are correct
...@@ -802,7 +805,7 @@ class CapaModule(CapaFields, XModule): ...@@ -802,7 +805,7 @@ class CapaModule(CapaFields, XModule):
event_info['correct_map'] = correct_map.get_dict() event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success event_info['success'] = success
event_info['attempts'] = self.attempts 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 if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_state_for_lcp()) self.system.psychometrics_handler(self.get_state_for_lcp())
...@@ -814,12 +817,92 @@ class CapaModule(CapaFields, XModule): ...@@ -814,12 +817,92 @@ class CapaModule(CapaFields, XModule):
'contents': html, '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): def save_problem(self, get):
''' """
Save the passed in answers. Save the passed in answers.
Returns a dict { 'success' : bool, ['error' : error-msg]}, Returns a dict { 'success' : bool, 'msg' : message }
with the error key only present if success is False. The message is informative on success, and an error message on failure.
''' """
event_info = dict() event_info = dict()
event_info['state'] = self.lcp.get_state() event_info['state'] = self.lcp.get_state()
event_info['problem_id'] = self.location.url() event_info['problem_id'] = self.location.url()
......
...@@ -58,7 +58,7 @@ class CombinedOpenEndedFields(object): ...@@ -58,7 +58,7 @@ class CombinedOpenEndedFields(object):
state = String(help="Which step within the current task that the student is on.", default="initial", state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state) scope=Scope.user_state)
student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, 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( ready_to_reset = Boolean(
help="If the problem is ready to be reset or not.", default=False, help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state scope=Scope.user_state
...@@ -66,7 +66,7 @@ class CombinedOpenEndedFields(object): ...@@ -66,7 +66,7 @@ class CombinedOpenEndedFields(object):
attempts = Integer( attempts = Integer(
display_name="Maximum Attempts", display_name="Maximum Attempts",
help="The number of times the student can try to answer this problem.", default=1, 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) is_graded = Boolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = Boolean( accept_file_upload = Boolean(
...@@ -89,7 +89,7 @@ class CombinedOpenEndedFields(object): ...@@ -89,7 +89,7 @@ class CombinedOpenEndedFields(object):
weight = Float( weight = Float(
display_name="Problem Weight", 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.", 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) markdown = String(help="Markdown source of this module", scope=Scope.settings)
......
...@@ -35,8 +35,11 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -35,8 +35,11 @@ class ConditionalModule(ConditionalFields, XModule):
<conditional> tag attributes: <conditional> tag attributes:
sources - location id of required modules, separated by ';' sources - location id of required modules, separated by ';'
completed - map to `is_completed` module method submitted - map to `is_submitted` module method.
(pressing RESET button makes this function to return False.)
attempted - map to `is_attempted` module method attempted - map to `is_attempted` module method
correct - map to `is_correct` module method
poll_answer - map to `poll_answer` module attribute poll_answer - map to `poll_answer` module attribute
voted - map to `voted` module attribute voted - map to `voted` module attribute
...@@ -70,8 +73,18 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -70,8 +73,18 @@ class ConditionalModule(ConditionalFields, XModule):
# value: <name of module attribute> # value: <name of module attribute>
conditions_map = { conditions_map = {
'poll_answer': 'poll_answer', # poll_question attr 'poll_answer': 'poll_answer', # poll_question attr
'completed': 'is_completed', # capa_problem attr
# problem was submitted (it can be wrong)
# if student will press reset button after that,
# state will be reverted
'submitted': 'is_submitted', # capa_problem attr
# if student attempted problem
'attempted': 'is_attempted', # capa_problem attr 'attempted': 'is_attempted', # capa_problem attr
# if problem is full points
'correct': 'is_correct',
'voted': 'voted' # poll_question attr 'voted': 'voted' # poll_question attr
} }
......
...@@ -77,10 +77,8 @@ class Date(ModelType): ...@@ -77,10 +77,8 @@ class Date(ModelType):
else: else:
return value.isoformat() return value.isoformat()
TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$') TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?) hour(?:s?))?(\s)?((?P<minutes>\d+?) minute(?:s)?)?(\s)?((?P<seconds>\d+?) second(?:s)?)?$')
class Timedelta(ModelType): class Timedelta(ModelType):
def from_json(self, time_str): def from_json(self, time_str):
""" """
......
...@@ -84,7 +84,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): ...@@ -84,7 +84,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
xml = html.fromstring(html_string) xml = html.fromstring(html_string)
#substitute plot, if presented # substitute plot, if presented
plot_div = '<div class="{element_class}_plot" id="{element_id}_plot" \ plot_div = '<div class="{element_class}_plot" id="{element_id}_plot" \
style="{style}"></div>' style="{style}"></div>'
plot_el = xml.xpath('//plot') plot_el = xml.xpath('//plot')
...@@ -95,7 +95,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule): ...@@ -95,7 +95,7 @@ class GraphicalSliderToolModule(GraphicalSliderToolFields, XModule):
element_id=self.html_id, element_id=self.html_id,
style=plot_el.get('style', "")))) style=plot_el.get('style', ""))))
#substitute sliders # substitute sliders
slider_div = '<div class="{element_class}_slider" \ slider_div = '<div class="{element_class}_slider" \
id="{element_id}_slider_{var}" \ id="{element_id}_slider_{var}" \
data-var="{var}" \ data-var="{var}" \
......
...@@ -57,7 +57,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -57,7 +57,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
if path.endswith('.html.xml'): if path.endswith('.html.xml'):
path = path[:-9] + '.html' # backcompat--look for html instead of xml path = path[:-9] + '.html' # backcompat--look for html instead of xml
if path.endswith('.html.html'): if path.endswith('.html.html'):
path = path[:-5] # some people like to include .html in filenames.. path = path[:-5] # some people like to include .html in filenames..
candidates = [] candidates = []
while os.sep in path: while os.sep in path:
candidates.append(path) candidates.append(path)
...@@ -100,9 +100,9 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -100,9 +100,9 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
pointer_path = "{category}/{url_path}".format(category='html', pointer_path = "{category}/{url_path}".format(category='html',
url_path=name_to_pathname(location.name)) url_path=name_to_pathname(location.name))
base = path(pointer_path).dirname() base = path(pointer_path).dirname()
#log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename)) # log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
filepath = "{base}/{name}.html".format(base=base, name=filename) filepath = "{base}/{name}.html".format(base=base, name=filename)
#log.debug("looking for html file for {0} at {1}".format(location, filepath)) # log.debug("looking for html file for {0} at {1}".format(location, filepath))
# VS[compat] # VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path, # TODO (cpennington): If the file doesn't exist at the right path,
...@@ -111,7 +111,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -111,7 +111,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
# online and has imported all current (fall 2012) courses from xml # online and has imported all current (fall 2012) courses from xml
if not system.resources_fs.exists(filepath): if not system.resources_fs.exists(filepath):
candidates = cls.backcompat_paths(filepath) candidates = cls.backcompat_paths(filepath)
#log.debug("candidates = {0}".format(candidates)) # log.debug("candidates = {0}".format(candidates))
for candidate in candidates: for candidate in candidates:
if system.resources_fs.exists(candidate): if system.resources_fs.exists(candidate):
filepath = candidate filepath = candidate
......
...@@ -196,7 +196,7 @@ class Location(_LocationBase): ...@@ -196,7 +196,7 @@ class Location(_LocationBase):
raise InvalidLocationError(location) raise InvalidLocationError(location)
if len(location) == 5: if len(location) == 5:
args = tuple(location) + (None, ) args = tuple(location) + (None,)
else: else:
args = tuple(location) args = tuple(location)
...@@ -415,7 +415,7 @@ class ModuleStoreBase(ModuleStore): ...@@ -415,7 +415,7 @@ class ModuleStoreBase(ModuleStore):
''' '''
Set up the error-tracking logic. Set up the error-tracking logic.
''' '''
self._location_errors = {} # location -> ErrorLog self._location_errors = {} # location -> ErrorLog
self.metadata_inheritance_cache = None self.metadata_inheritance_cache = None
self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes self.modulestore_update_signal = None # can be set by runtime to route notifications of datastore changes
...@@ -440,7 +440,7 @@ class ModuleStoreBase(ModuleStore): ...@@ -440,7 +440,7 @@ class ModuleStoreBase(ModuleStore):
""" """
# check that item is present and raise the promised exceptions if needed # check that item is present and raise the promised exceptions if needed
# TODO (vshnayder): post-launch, make errors properties of items # TODO (vshnayder): post-launch, make errors properties of items
#self.get_item(location) # self.get_item(location)
errorlog = self._get_errorlog(location) errorlog = self._get_errorlog(location)
return errorlog.errors return errorlog.errors
......
...@@ -15,14 +15,14 @@ def as_draft(location): ...@@ -15,14 +15,14 @@ def as_draft(location):
""" """
Returns the Location that is the draft for `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): def as_published(location):
""" """
Returns the Location that is the published version for `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): def wrap_draft(item):
...@@ -32,7 +32,7 @@ def wrap_draft(item): ...@@ -32,7 +32,7 @@ def wrap_draft(item):
non-draft location in either case non-draft location in either case
""" """
setattr(item, 'is_draft', item.location.revision == DRAFT) setattr(item, 'is_draft', item.location.revision == DRAFT)
item.location = item.location._replace(revision=None) item.location = item.location.replace(revision=None)
return item return item
...@@ -234,7 +234,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -234,7 +234,7 @@ class DraftModuleStore(ModuleStoreBase):
# always return the draft - if available # always return the draft - if available
for draft in to_process_drafts: for draft in to_process_drafts:
draft_loc = Location(draft["_id"]) 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 # does non-draft exist in the collection
# if so, replace it # if so, replace it
......
...@@ -307,7 +307,7 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -307,7 +307,7 @@ class MongoModuleStore(ModuleStoreBase):
location = Location(result['_id']) location = Location(result['_id'])
# We need to collate between draft and non-draft # We need to collate between draft and non-draft
# i.e. draft verticals can have children which are not in non-draft versions # 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() location_url = location.url()
if location_url in results_by_url: if location_url in results_by_url:
existing_children = results_by_url[location_url].get('definition', {}).get('children', []) existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
......
...@@ -19,18 +19,18 @@ log = logging.getLogger("mitx.courseware") ...@@ -19,18 +19,18 @@ log = logging.getLogger("mitx.courseware")
# attempts specified in xml definition overrides this. # attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1 MAX_ATTEMPTS = 1
#The highest score allowed for the overall xmodule and for each rubric point # The highest score allowed for the overall xmodule and for each rubric point
MAX_SCORE_ALLOWED = 50 MAX_SCORE_ALLOWED = 50
#If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress # If true, default behavior is to score module as a practice problem. Otherwise, no grade at all is shown in progress
#Metadata overrides this. # Metadata overrides this.
IS_SCORED = False IS_SCORED = False
#If true, then default behavior is to require a file upload or pasted link from a student for this problem. # If true, then default behavior is to require a file upload or pasted link from a student for this problem.
#Metadata overrides this. # Metadata overrides this.
ACCEPT_FILE_UPLOAD = False 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"] TRUE_DICT = ["True", True, "TRUE", "true"]
HUMAN_TASK_TYPE = { HUMAN_TASK_TYPE = {
...@@ -38,8 +38,8 @@ HUMAN_TASK_TYPE = { ...@@ -38,8 +38,8 @@ HUMAN_TASK_TYPE = {
'openended': "edX Assessment", 'openended': "edX Assessment",
} }
#Default value that controls whether or not to skip basic spelling checks in the controller # Default value that controls whether or not to skip basic spelling checks in the controller
#Metadata overrides this # Metadata overrides this
SKIP_BASIC_CHECKS = False SKIP_BASIC_CHECKS = False
...@@ -74,7 +74,7 @@ class CombinedOpenEndedV1Module(): ...@@ -74,7 +74,7 @@ class CombinedOpenEndedV1Module():
INTERMEDIATE_DONE = 'intermediate_done' INTERMEDIATE_DONE = 'intermediate_done'
DONE = 'done' DONE = 'done'
#Where the templates live for this problem # Where the templates live for this problem
TEMPLATE_DIR = "combinedopenended" TEMPLATE_DIR = "combinedopenended"
def __init__(self, system, location, definition, descriptor, def __init__(self, system, location, definition, descriptor,
...@@ -118,21 +118,21 @@ class CombinedOpenEndedV1Module(): ...@@ -118,21 +118,21 @@ class CombinedOpenEndedV1Module():
self.instance_state = instance_state self.instance_state = instance_state
self.display_name = instance_state.get('display_name', "Open Ended") 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) system.set('location', location)
self.system = system 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) 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', []) 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.state = instance_state.get('state', self.INITIAL)
self.student_attempts = instance_state.get('student_attempts', 0) self.student_attempts = instance_state.get('student_attempts', 0)
self.weight = instance_state.get('weight', 1) 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.ready_to_reset = instance_state.get('ready_to_reset', False)
self.attempts = self.instance_state.get('attempts', MAX_ATTEMPTS) self.attempts = self.instance_state.get('attempts', MAX_ATTEMPTS)
self.is_scored = self.instance_state.get('is_graded', IS_SCORED) in TRUE_DICT self.is_scored = self.instance_state.get('is_graded', IS_SCORED) in TRUE_DICT
...@@ -153,7 +153,7 @@ class CombinedOpenEndedV1Module(): ...@@ -153,7 +153,7 @@ class CombinedOpenEndedV1Module():
rubric_string = stringify_children(definition['rubric']) rubric_string = stringify_children(definition['rubric'])
self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED) self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED)
#Static data is passed to the child modules to render # Static data is passed to the child modules to render
self.static_data = { self.static_data = {
'max_score': self._max_score, 'max_score': self._max_score,
'max_attempts': self.attempts, 'max_attempts': self.attempts,
...@@ -243,11 +243,11 @@ class CombinedOpenEndedV1Module(): ...@@ -243,11 +243,11 @@ class CombinedOpenEndedV1Module():
self.current_task_descriptor = children['descriptors'][current_task_type](self.system) 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) etree_xml = etree.fromstring(self.current_task_xml)
#This sends the etree_xml object through the descriptor module of the current task, and # This sends the etree_xml object through the descriptor module of the current task, and
#returns the xml parsed by the descriptor # returns the xml parsed by the descriptor
self.current_task_parsed_xml = self.current_task_descriptor.definition_from_xml(etree_xml, self.system) 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: if current_task_state is None and self.current_task_number == 0:
self.current_task = child_task_module(self.system, self.location, self.current_task = child_task_module(self.system, self.location,
...@@ -293,8 +293,9 @@ class CombinedOpenEndedV1Module(): ...@@ -293,8 +293,9 @@ class CombinedOpenEndedV1Module():
if self.current_task_number > 0: if self.current_task_number > 0:
last_response_data = self.get_last_response(self.current_task_number - 1) last_response_data = self.get_last_response(self.current_task_number - 1)
current_response_data = self.get_current_attributes(self.current_task_number) current_response_data = self.get_current_attributes(self.current_task_number)
if (current_response_data['min_score_to_attempt'] > last_response_data['score'] 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.state = self.DONE
self.ready_to_reset = True self.ready_to_reset = True
...@@ -307,7 +308,7 @@ class CombinedOpenEndedV1Module(): ...@@ -307,7 +308,7 @@ class CombinedOpenEndedV1Module():
Output: A dictionary that can be rendered into the combined open ended template. Output: A dictionary that can be rendered into the combined open ended template.
""" """
task_html = self.get_html_base() task_html = self.get_html_base()
#set context variables and render template # set context variables and render template
context = { context = {
'items': [{'content': task_html}], 'items': [{'content': task_html}],
...@@ -499,7 +500,6 @@ class CombinedOpenEndedV1Module(): ...@@ -499,7 +500,6 @@ class CombinedOpenEndedV1Module():
""" """
changed = self.update_task_states() changed = self.update_task_states()
if changed: if changed:
#return_html=self.get_html()
pass pass
return return_html return return_html
...@@ -730,15 +730,15 @@ class CombinedOpenEndedV1Module(): ...@@ -730,15 +730,15 @@ class CombinedOpenEndedV1Module():
max_score = None max_score = None
score = None score = None
if self.is_scored and self.weight is not 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 = [] score_mat = []
for i in xrange(0, len(self.task_states)): 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) last_response = self.get_last_response(i)
max_score = last_response.get('max_score', None) max_score = last_response.get('max_score', None)
score = last_response.get('all_scores', None) score = last_response.get('all_scores', None)
if score is not 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)): for z in xrange(0, len(score)):
if score[z] is None: if score[z] is None:
score[z] = 0 score[z] = 0
...@@ -746,19 +746,19 @@ class CombinedOpenEndedV1Module(): ...@@ -746,19 +746,19 @@ class CombinedOpenEndedV1Module():
score_mat.append(score) score_mat.append(score)
if len(score_mat) > 0: if len(score_mat) > 0:
#Currently, assume that the final step is the correct one, and that those are the final scores. # 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 # 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. # TODO: better final score handling.
scores = score_mat[-1] scores = score_mat[-1]
score = max(scores) score = max(scores)
else: else:
score = 0 score = 0
if max_score is not None: 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) max_score *= float(self.weight)
else: else:
#Without a max_score, we cannot have a score! # Without a max_score, we cannot have a score!
score = None score = None
score_dict = { score_dict = {
...@@ -833,7 +833,7 @@ class CombinedOpenEndedV1Descriptor(): ...@@ -833,7 +833,7 @@ class CombinedOpenEndedV1Descriptor():
expected_children = ['task', 'rubric', 'prompt'] expected_children = ['task', 'rubric', 'prompt']
for child in expected_children: for child in expected_children:
if len(xml_object.xpath(child)) == 0: if len(xml_object.xpath(child)) == 0:
#This is a staff_facing_error # This is a staff_facing_error
raise ValueError( raise ValueError(
"Combined Open Ended definition must include at least one '{0}' tag. Contact the learning sciences group for assistance. {1}".format( "Combined Open Ended definition must include at least one '{0}' tag. Contact the learning sciences group for assistance. {1}".format(
child, xml_object)) child, xml_object))
...@@ -848,6 +848,7 @@ class CombinedOpenEndedV1Descriptor(): ...@@ -848,6 +848,7 @@ class CombinedOpenEndedV1Descriptor():
return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')} return {'task_xml': parse_task('task'), 'prompt': parse('prompt'), 'rubric': parse('rubric')}
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
'''Return an xml element representing this definition.''' '''Return an xml element representing this definition.'''
elt = etree.Element('combinedopenended') elt = etree.Element('combinedopenended')
......
...@@ -54,7 +54,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -54,7 +54,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
@param system: Modulesystem @param system: Modulesystem
@return: Rendered HTML @return: Rendered HTML
""" """
#set context variables and render template # set context variables and render template
if self.child_state != self.INITIAL: if self.child_state != self.INITIAL:
latest = self.latest_answer() latest = self.latest_answer()
previous_answer = latest if latest is not None else '' previous_answer = latest if latest is not None else ''
...@@ -93,9 +93,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -93,9 +93,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
} }
if dispatch not in handlers: 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)) 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}) return json.dumps({'error': 'Error handling action. Please try again.', 'success': False})
before = self.get_progress() before = self.get_progress()
...@@ -129,7 +129,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -129,7 +129,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
elif self.child_state in (self.POST_ASSESSMENT, self.DONE): elif self.child_state in (self.POST_ASSESSMENT, self.DONE):
context['read_only'] = True context['read_only'] = True
else: 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)) 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) return system.render_template('{0}/self_assessment_rubric.html'.format(self.TEMPLATE_DIR), context)
...@@ -155,7 +155,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -155,7 +155,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
elif self.child_state == self.DONE: elif self.child_state == self.DONE:
context['read_only'] = True context['read_only'] = True
else: 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)) 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) return system.render_template('{0}/self_assessment_hint.html'.format(self.TEMPLATE_DIR), context)
...@@ -190,10 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -190,10 +190,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
self.new_history_entry(get['student_answer']) self.new_history_entry(get['student_answer'])
self.change_state(self.ASSESSING) self.change_state(self.ASSESSING)
else: else:
#Error message already defined # Error message already defined
success = False success = False
else: 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." 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 { return {
...@@ -227,12 +227,12 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -227,12 +227,12 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
for i in xrange(0, len(score_list)): for i in xrange(0, len(score_list)):
score_list[i] = int(score_list[i]) score_list[i] = int(score_list[i])
except ValueError: 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.") 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."} 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_score(score)
self.record_latest_post_assessment(json.dumps(score_list)) self.record_latest_post_assessment(json.dumps(score_list))
...@@ -272,7 +272,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -272,7 +272,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
try: try:
rubric_scores = json.loads(latest_post_assessment) rubric_scores = json.loads(latest_post_assessment)
except: 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)) log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment))
rubric_scores = [] rubric_scores = []
return [rubric_scores] return [rubric_scores]
...@@ -306,7 +306,7 @@ class SelfAssessmentDescriptor(): ...@@ -306,7 +306,7 @@ class SelfAssessmentDescriptor():
expected_children = [] expected_children = []
for child in expected_children: for child in expected_children:
if len(xml_object.xpath(child)) != 1: if len(xml_object.xpath(child)) != 1:
#This is a staff_facing_error # This is a staff_facing_error
raise ValueError( raise ValueError(
"Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format( "Self assessment definition must include exactly one '{0}' tag. Contact the learning sciences group for assistance.".format(
child)) child))
......
...@@ -62,7 +62,7 @@ class SequenceModule(SequenceFields, XModule): ...@@ -62,7 +62,7 @@ class SequenceModule(SequenceFields, XModule):
progress = reduce(Progress.add_counts, progresses) progress = reduce(Progress.add_counts, progresses)
return progress return progress
def handle_ajax(self, dispatch, get): # TODO: bounds checking def handle_ajax(self, dispatch, get): # TODO: bounds checking
''' get = request.POST instance ''' ''' get = request.POST instance '''
if dispatch == 'goto_position': if dispatch == 'goto_position':
self.position = int(get['position']) self.position = int(get['position'])
......
...@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content ...@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content
that is defined by XModules and XModuleDescriptors (javascript and css) that is defined by XModules and XModuleDescriptors (javascript and css)
""" """
import logging
import hashlib import hashlib
import os import os
import errno import errno
...@@ -15,6 +16,9 @@ from path import path ...@@ -15,6 +16,9 @@ from path import path
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
LOG = logging.getLogger(__name__)
def write_module_styles(output_root): def write_module_styles(output_root):
return _write_styles('.xmodule_display', output_root, _list_modules()) return _write_styles('.xmodule_display', output_root, _list_modules())
...@@ -121,18 +125,32 @@ def _write_js(output_root, classes): ...@@ -121,18 +125,32 @@ def _write_js(output_root, classes):
type=filetype) type=filetype)
contents[filename] = fragment contents[filename] = fragment
_write_files(output_root, contents) _write_files(output_root, contents, {'.coffee': '.js'})
return [output_root / filename for filename in contents.keys()] return [output_root / filename for filename in contents.keys()]
def _write_files(output_root, contents): def _write_files(output_root, contents, generated_suffix_map=None):
_ensure_dir(output_root) _ensure_dir(output_root)
for extra_file in set(output_root.files()) - set(contents.keys()): to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys())
extra_file.remove_p()
if generated_suffix_map:
for output_file in contents.keys():
for suffix, generated_suffix in generated_suffix_map.items():
if output_file.endswith(suffix):
to_delete.discard(output_file.replace(suffix, generated_suffix))
for extra_file in to_delete:
(output_root / extra_file).remove_p()
for filename, file_content in contents.iteritems(): for filename, file_content in contents.iteritems():
(output_root / filename).write_bytes(file_content) output_file = output_root / filename
if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest():
LOG.debug("Writing %s", output_file)
output_file.write_bytes(file_content)
else:
LOG.debug("%s unchanged, skipping", output_file)
def main(): def main():
......
...@@ -55,7 +55,7 @@ class CustomTagDescriptor(RawDescriptor): ...@@ -55,7 +55,7 @@ class CustomTagDescriptor(RawDescriptor):
params = dict(xmltree.items()) params = dict(xmltree.items())
# cdodge: look up the template as a module # 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 = modulestore().get_instance(system.course_id, template_loc)
template_module_data = template_module.data template_module_data = template_module.data
......
...@@ -29,14 +29,14 @@ open_ended_grading_interface = { ...@@ -29,14 +29,14 @@ open_ended_grading_interface = {
} }
def test_system(): def get_test_system():
""" """
Construct a test ModuleSystem instance. Construct a test ModuleSystem instance.
By default, the render_template() method simply returns the repr of the By default, the render_template() method simply returns the repr of the
context it is passed. You can override this behavior by monkey patching:: context it is passed. You can override this behavior by monkey patching::
system = test_system() system = get_test_system()
system.render_template = my_render_func system.render_template = my_render_func
where `my_render_func` is a function of the form my_render_func(template, context). where `my_render_func` is a function of the form my_render_func(template, context).
......
...@@ -8,7 +8,7 @@ from mock import Mock ...@@ -8,7 +8,7 @@ from mock import Mock
from xmodule.annotatable_module import AnnotatableModule from xmodule.annotatable_module import AnnotatableModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from . import test_system from . import get_test_system
class AnnotatableModuleTestCase(unittest.TestCase): class AnnotatableModuleTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"]) location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"])
...@@ -32,7 +32,7 @@ class AnnotatableModuleTestCase(unittest.TestCase): ...@@ -32,7 +32,7 @@ class AnnotatableModuleTestCase(unittest.TestCase):
module_data = {'data': sample_xml, 'location': location} module_data = {'data': sample_xml, 'location': location}
def setUp(self): def setUp(self):
self.annotatable = AnnotatableModule(test_system(), self.descriptor, self.module_data) self.annotatable = AnnotatableModule(get_test_system(), self.descriptor, self.module_data)
def test_annotation_data_attr(self): def test_annotation_data_attr(self):
el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>') el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
......
...@@ -17,8 +17,9 @@ from xmodule.modulestore import Location ...@@ -17,8 +17,9 @@ from xmodule.modulestore import Location
from django.http import QueryDict from django.http import QueryDict
from . import test_system from . import get_test_system
from pytz import UTC from pytz import UTC
from capa.correctmap import CorrectMap
class CapaFactory(object): class CapaFactory(object):
...@@ -111,7 +112,7 @@ class CapaFactory(object): ...@@ -111,7 +112,7 @@ class CapaFactory(object):
# since everything else is a string. # since everything else is a string.
model_data['attempts'] = int(attempts) model_data['attempts'] = int(attempts)
system = test_system() system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
module = CapaModule(system, descriptor, model_data) module = CapaModule(system, descriptor, model_data)
...@@ -597,6 +598,85 @@ class CapaModuleTest(unittest.TestCase): ...@@ -597,6 +598,85 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the problem was NOT reset # Expect that the problem was NOT reset
self.assertTrue('success' in result and not result['success']) 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): def test_save_problem(self):
module = CapaFactory.create(done=False) module = CapaFactory.create(done=False)
...@@ -922,7 +1002,7 @@ class CapaModuleTest(unittest.TestCase): ...@@ -922,7 +1002,7 @@ class CapaModuleTest(unittest.TestCase):
# is asked to render itself as HTML # is asked to render itself as HTML
module.lcp.get_html = Mock(side_effect=Exception("Test")) module.lcp.get_html = Mock(side_effect=Exception("Test"))
# Stub out the test_system rendering function # Stub out the get_test_system rendering function
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>") module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
# Turn off DEBUG # Turn off DEBUG
......
...@@ -18,7 +18,7 @@ import logging ...@@ -18,7 +18,7 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from . import test_system from . import get_test_system
ORG = 'edX' ORG = 'edX'
COURSE = 'open_ended' # name of directory with course data COURSE = 'open_ended' # name of directory with course data
...@@ -68,7 +68,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -68,7 +68,7 @@ class OpenEndedChildTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location, self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -192,7 +192,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -192,7 +192,7 @@ class OpenEndedModuleTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
...@@ -367,7 +367,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -367,7 +367,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]} definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2) full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
descriptor = Mock(data=full_definition) descriptor = Mock(data=full_definition)
test_system = test_system() test_system = get_test_system()
combinedoe_container = CombinedOpenEndedModule( combinedoe_container = CombinedOpenEndedModule(
test_system, test_system,
descriptor, descriptor,
...@@ -493,7 +493,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -493,7 +493,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
hint = "blah" hint = "blah"
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.xqueue['interface'] = Mock( self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"]) send_to_queue=Mock(side_effect=[1, "queued"])
) )
...@@ -569,6 +569,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -569,6 +569,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
#Mock a student submitting an assessment #Mock a student submitting an assessment
assessment_dict = MockQueryDict() assessment_dict = MockQueryDict()
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment}) assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
#from nose.tools import set_trace; set_trace()
module.handle_ajax("save_assessment", assessment_dict) module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0]) task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
......
...@@ -15,12 +15,12 @@ from xmodule.tests.test_export import DATA_DIR ...@@ -15,12 +15,12 @@ from xmodule.tests.test_export import DATA_DIR
ORG = 'test_org' ORG = 'test_org'
COURSE = 'conditional' # name of directory with course data COURSE = 'conditional' # name of directory with course data
from . import test_system from . import get_test_system
class DummySystem(ImportSystem): 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): def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules) xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
...@@ -41,7 +41,8 @@ class DummySystem(ImportSystem): ...@@ -41,7 +41,8 @@ class DummySystem(ImportSystem):
) )
def render_template(self, template, context): def render_template(self, template, context):
raise Exception("Shouldn't be called") raise Exception("Shouldn't be called")
class ConditionalFactory(object): class ConditionalFactory(object):
""" """
...@@ -93,7 +94,7 @@ class ConditionalFactory(object): ...@@ -93,7 +94,7 @@ class ConditionalFactory(object):
# return dict: # return dict:
return {'cond_module': cond_module, return {'cond_module': cond_module,
'source_module': source_module, 'source_module': source_module,
'child_module': child_module } 'child_module': child_module}
class ConditionalModuleBasicTest(unittest.TestCase): class ConditionalModuleBasicTest(unittest.TestCase):
...@@ -103,21 +104,20 @@ class ConditionalModuleBasicTest(unittest.TestCase): ...@@ -103,21 +104,20 @@ class ConditionalModuleBasicTest(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
def test_icon_class(self): def test_icon_class(self):
'''verify that get_icon_class works independent of condition satisfaction''' '''verify that get_icon_class works independent of condition satisfaction'''
modules = ConditionalFactory.create(self.test_system) modules = ConditionalFactory.create(self.test_system)
for attempted in ["false", "true"]: 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['source_module'].is_attempted = attempted
modules['child_module'].get_icon_class = lambda: icon_class modules['child_module'].get_icon_class = lambda: icon_class
self.assertEqual(modules['cond_module'].get_icon_class(), icon_class) self.assertEqual(modules['cond_module'].get_icon_class(), icon_class)
def test_get_html(self): def test_get_html(self):
modules = ConditionalFactory.create(self.test_system) modules = ConditionalFactory.create(self.test_system)
# because test_system returns the repr of the context dict passed to render_template, # because get_test_system returns the repr of the context dict passed to render_template,
# we reverse it here # we reverse it here
html = modules['cond_module'].get_html() html = modules['cond_module'].get_html()
html_dict = literal_eval(html) html_dict = literal_eval(html)
...@@ -161,7 +161,7 @@ class ConditionalModuleXmlTest(unittest.TestCase): ...@@ -161,7 +161,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
return DummySystem(load_error_modules) return DummySystem(load_error_modules)
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = get_test_system()
def get_course(self, name): def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error.""" """Get a test course by directory name. If there's more than one, error."""
...@@ -224,4 +224,3 @@ class ConditionalModuleXmlTest(unittest.TestCase): ...@@ -224,4 +224,3 @@ class ConditionalModuleXmlTest(unittest.TestCase):
print "post-attempt ajax: ", ajax print "post-attempt ajax: ", ajax
html = ajax['html'] html = ajax['html']
self.assertTrue(any(['This is a secret' in item for item in html])) self.assertTrue(any(['This is a secret' in item for item in html]))
...@@ -2,25 +2,30 @@ ...@@ -2,25 +2,30 @@
Tests for ErrorModule and NonStaffErrorModule Tests for ErrorModule and NonStaffErrorModule
""" """
import unittest import unittest
from xmodule.tests import test_system from xmodule.tests import get_test_system
import xmodule.error_module as error_module import xmodule.error_module as error_module
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from mock import MagicMock from mock import MagicMock
class TestErrorModule(unittest.TestCase): class SetupTestErrorModules():
"""
Tests for ErrorModule and ErrorDescriptor
"""
def setUp(self): def setUp(self):
self.system = test_system() self.system = get_test_system()
self.org = "org" self.org = "org"
self.course = "course" self.course = "course"
self.location = Location(['i4x', self.org, self.course, None, None]) self.location = Location(['i4x', self.org, self.course, None, None])
self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>" self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>"
self.error_msg = "Error" self.error_msg = "Error"
class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
"""
Tests for ErrorModule and ErrorDescriptor
"""
def setUp(self):
SetupTestErrorModules.setUp(self)
def test_error_module_xml_rendering(self): def test_error_module_xml_rendering(self):
descriptor = error_module.ErrorDescriptor.from_xml( descriptor = error_module.ErrorDescriptor.from_xml(
self.valid_xml, self.system, self.org, self.course, self.error_msg) self.valid_xml, self.system, self.org, self.course, self.error_msg)
...@@ -45,10 +50,12 @@ class TestErrorModule(unittest.TestCase): ...@@ -45,10 +50,12 @@ class TestErrorModule(unittest.TestCase):
self.assertIn(repr(descriptor), context_repr) self.assertIn(repr(descriptor), context_repr)
class TestNonStaffErrorModule(TestErrorModule): class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
""" """
Tests for NonStaffErrorModule and NonStaffErrorDescriptor Tests for NonStaffErrorModule and NonStaffErrorDescriptor
""" """
def setUp(self):
SetupTestErrorModules.setUp(self)
def test_non_staff_error_module_create(self): def test_non_staff_error_module_create(self):
descriptor = error_module.NonStaffErrorDescriptor.from_xml( descriptor = error_module.NonStaffErrorDescriptor.from_xml(
......
...@@ -5,7 +5,7 @@ from mock import Mock ...@@ -5,7 +5,7 @@ from mock import Mock
from xmodule.html_module import HtmlModule from xmodule.html_module import HtmlModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from . import test_system from . import get_test_system
class HtmlModuleSubstitutionTestCase(unittest.TestCase): class HtmlModuleSubstitutionTestCase(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
...@@ -13,7 +13,7 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase): ...@@ -13,7 +13,7 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
def test_substitution_works(self): def test_substitution_works(self):
sample_xml = '''%%USER_ID%%''' sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml} module_data = {'data': sample_xml}
module_system = test_system() module_system = get_test_system()
module = HtmlModule(module_system, self.descriptor, module_data) module = HtmlModule(module_system, self.descriptor, module_data)
self.assertEqual(module.get_html(), str(module_system.anonymous_student_id)) self.assertEqual(module.get_html(), str(module_system.anonymous_student_id))
...@@ -25,14 +25,14 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase): ...@@ -25,14 +25,14 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
</html> </html>
''' '''
module_data = {'data': sample_xml} module_data = {'data': sample_xml}
module = HtmlModule(test_system(), self.descriptor, module_data) module = HtmlModule(get_test_system(), self.descriptor, module_data)
self.assertEqual(module.get_html(), sample_xml) self.assertEqual(module.get_html(), sample_xml)
def test_substitution_without_anonymous_student_id(self): def test_substitution_without_anonymous_student_id(self):
sample_xml = '''%%USER_ID%%''' sample_xml = '''%%USER_ID%%'''
module_data = {'data': sample_xml} module_data = {'data': sample_xml}
module_system = test_system() module_system = get_test_system()
module_system.anonymous_student_id = None module_system.anonymous_student_id = None
module = HtmlModule(module_system, self.descriptor, module_data) module = HtmlModule(module_system, self.descriptor, module_data)
self.assertEqual(module.get_html(), sample_xml) self.assertEqual(module.get_html(), sample_xml)
......
...@@ -8,7 +8,7 @@ import unittest ...@@ -8,7 +8,7 @@ import unittest
from xmodule.poll_module import PollDescriptor from xmodule.poll_module import PollDescriptor
from xmodule.conditional_module import ConditionalDescriptor from xmodule.conditional_module import ConditionalDescriptor
from xmodule.word_cloud_module import WordCloudDescriptor from xmodule.word_cloud_module import WordCloudDescriptor
from xmodule.tests import test_system from xmodule.tests import get_test_system
class PostData: class PostData:
"""Class which emulate postdata.""" """Class which emulate postdata."""
...@@ -30,7 +30,7 @@ class LogicTest(unittest.TestCase): ...@@ -30,7 +30,7 @@ class LogicTest(unittest.TestCase):
"""Empty object.""" """Empty object."""
pass pass
self.system = test_system() self.system = get_test_system()
self.descriptor = EmptyClass() self.descriptor = EmptyClass()
self.xmodule_class = self.descriptor_class.module_class self.xmodule_class = self.descriptor_class.module_class
......
import unittest import unittest
from xmodule.modulestore import Location from xmodule.modulestore import Location
from .import test_system from .import get_test_system
from test_util_open_ended import MockQueryDict, DummyModulestore from test_util_open_ended import MockQueryDict, DummyModulestore
import json import json
...@@ -39,7 +39,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore): ...@@ -39,7 +39,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Create a peer grading module from a test system Create a peer grading module from a test system
@return: @return:
""" """
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None self.test_system.open_ended_grading_interface = None
self.setup_modulestore(COURSE) self.setup_modulestore(COURSE)
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE) self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
...@@ -151,10 +151,10 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore): ...@@ -151,10 +151,10 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
Create a peer grading module from a test system Create a peer grading module from a test system
@return: @return:
""" """
self.test_system = test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None self.test_system.open_ended_grading_interface = None
self.setup_modulestore(COURSE) self.setup_modulestore(COURSE)
def test_metadata_load(self): def test_metadata_load(self):
peer_grading = self.get_module_from_location(self.problem_location, COURSE) peer_grading = self.get_module_from_location(self.problem_location, COURSE)
self.assertEqual(peer_grading.closed(), False) self.assertEqual(peer_grading.closed(), False)
\ No newline at end of file
...@@ -5,7 +5,7 @@ import unittest ...@@ -5,7 +5,7 @@ import unittest
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule import x_module from xmodule import x_module
from . import test_system from . import get_test_system
class ProgressTest(unittest.TestCase): class ProgressTest(unittest.TestCase):
...@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase): ...@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase):
''' '''
def test_xmodule_default(self): def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None''' '''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(test_system(), None, {'location': 'a://b/c/d/e'}) xm = x_module.XModule(get_test_system(), None, {'location': 'a://b/c/d/e'})
p = xm.get_progress() p = xm.get_progress()
self.assertEqual(p, None) self.assertEqual(p, None)
...@@ -6,7 +6,7 @@ from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssess ...@@ -6,7 +6,7 @@ from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssess
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
from . import test_system from . import get_test_system
import test_util_open_ended import test_util_open_ended
...@@ -51,7 +51,7 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -51,7 +51,7 @@ class SelfAssessmentTest(unittest.TestCase):
'skip_basic_checks': False, 'skip_basic_checks': False,
} }
self.module = SelfAssessmentModule(test_system(), self.location, self.module = SelfAssessmentModule(get_test_system(), self.location,
self.definition, self.definition,
self.descriptor, self.descriptor,
static_data) static_data)
......
from .import test_system from .import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
from xmodule.tests.test_export import DATA_DIR from xmodule.tests.test_export import DATA_DIR
...@@ -37,7 +37,7 @@ class DummyModulestore(object): ...@@ -37,7 +37,7 @@ class DummyModulestore(object):
""" """
A mixin that allows test classes to have convenience functions to get a module given a location A mixin that allows test classes to have convenience functions to get a module given a location
""" """
test_system = test_system() get_test_system = get_test_system()
def setup_modulestore(self, name): def setup_modulestore(self, name):
self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name]) self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
......
...@@ -20,7 +20,7 @@ from lxml import etree ...@@ -20,7 +20,7 @@ from lxml import etree
from xmodule.video_module import VideoDescriptor, VideoModule from xmodule.video_module import VideoDescriptor, VideoModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import test_system from xmodule.tests import get_test_system
from xmodule.tests.test_logic import LogicTest from xmodule.tests.test_logic import LogicTest
...@@ -51,7 +51,7 @@ class VideoFactory(object): ...@@ -51,7 +51,7 @@ class VideoFactory(object):
descriptor = Mock(weight="1") descriptor = Mock(weight="1")
system = test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
module = VideoModule(system, descriptor, model_data) module = VideoModule(system, descriptor, model_data)
......
...@@ -6,7 +6,7 @@ from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List ...@@ -6,7 +6,7 @@ from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
from xmodule.fields import Date, Timedelta from xmodule.fields import Date, Timedelta
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
import unittest import unittest
from .import test_system from .import get_test_system
from nose.tools import assert_equals from nose.tools import assert_equals
from mock import Mock from mock import Mock
...@@ -140,7 +140,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -140,7 +140,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
# Start of helper methods # Start of helper methods
def get_xml_editable_fields(self, model_data): def get_xml_editable_fields(self, model_data):
system = test_system() system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return XmlDescriptor(runtime=system, model_data=model_data).editable_metadata_fields return XmlDescriptor(runtime=system, model_data=model_data).editable_metadata_fields
...@@ -152,7 +152,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -152,7 +152,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
non_editable_fields.append(TestModuleDescriptor.due) non_editable_fields.append(TestModuleDescriptor.due)
return non_editable_fields return non_editable_fields
system = test_system() system = get_test_system()
system.render_template = Mock(return_value="<div>Test Template HTML</div>") system.render_template = Mock(return_value="<div>Test Template HTML</div>")
return TestModuleDescriptor(runtime=system, model_data=model_data) return TestModuleDescriptor(runtime=system, model_data=model_data)
......
...@@ -15,7 +15,7 @@ from xblock.core import XBlock, Scope, String, Integer, Float, ModelType ...@@ -15,7 +15,7 @@ from xblock.core import XBlock, Scope, String, Integer, Float, ModelType
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def dummy_track(event_type, event): def dummy_track(_event_type, _event):
pass pass
...@@ -231,7 +231,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock): ...@@ -231,7 +231,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
''' '''
return self.icon_class return self.icon_class
### Functions used in the LMS # Functions used in the LMS
def get_score(self): def get_score(self):
""" """
...@@ -272,7 +272,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock): ...@@ -272,7 +272,7 @@ class XModule(XModuleFields, HTMLSnippet, XBlock):
''' '''
return None return None
def handle_ajax(self, dispatch, get): def handle_ajax(self, _dispatch, _get):
''' dispatch is last part of the URL. ''' dispatch is last part of the URL.
get is a dictionary-like object ''' get is a dictionary-like object '''
return "" return ""
...@@ -647,13 +647,13 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -647,13 +647,13 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# 1. A select editor for fields with a list of possible values (includes Booleans). # 1. A select editor for fields with a list of possible values (includes Booleans).
# 2. Number editors for integers and floats. # 2. Number editors for integers and floats.
# 3. A generic string editor for anything else (editing JSON representation of the value). # 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) values = [] if field.values is None else copy.deepcopy(field.values)
if isinstance(values, tuple): if isinstance(values, tuple):
values = list(values) values = list(values)
if isinstance(values, list): if isinstance(values, list):
if len(values) > 0: if len(values) > 0:
type = "Select" editor_type = "Select"
for index, choice in enumerate(values): for index, choice in enumerate(values):
json_choice = copy.deepcopy(choice) json_choice = copy.deepcopy(choice)
if isinstance(json_choice, dict) and 'value' in json_choice: if isinstance(json_choice, dict) and 'value' in json_choice:
...@@ -662,11 +662,11 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -662,11 +662,11 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
json_choice = field.to_json(json_choice) json_choice = field.to_json(json_choice)
values[index] = json_choice values[index] = json_choice
elif isinstance(field, Integer): elif isinstance(field, Integer):
type = "Integer" editor_type = "Integer"
elif isinstance(field, Float): elif isinstance(field, Float):
type = "Float" editor_type = "Float"
metadata_fields[field.name] = {'field_name': field.name, metadata_fields[field.name] = {'field_name': field.name,
'type': type, 'type': editor_type,
'display_name': field.display_name, 'display_name': field.display_name,
'value': field.to_json(value), 'value': field.to_json(value),
'options': values, 'options': values,
...@@ -862,7 +862,7 @@ class ModuleSystem(object): ...@@ -862,7 +862,7 @@ class ModuleSystem(object):
class DoNothingCache(object): class DoNothingCache(object):
"""A duck-compatible object to use in ModuleSystem when there's no cache.""" """A duck-compatible object to use in ModuleSystem when there's no cache."""
def get(self, key): def get(self, _key):
return None return None
def set(self, key, value, timeout=None): def set(self, key, value, timeout=None):
......
...@@ -56,7 +56,6 @@ def get_metadata_from_xml(xml_object, remove=True): ...@@ -56,7 +56,6 @@ def get_metadata_from_xml(xml_object, remove=True):
if meta is None: if meta is None:
return '' return ''
dmdata = meta.text dmdata = meta.text
#log.debug('meta for %s loaded: %s' % (xml_object,dmdata))
if remove: if remove:
xml_object.remove(meta) xml_object.remove(meta)
return dmdata return dmdata
......
...@@ -3,6 +3,11 @@ describe 'Logger', -> ...@@ -3,6 +3,11 @@ describe 'Logger', ->
expect(window.log_event).toBe Logger.log expect(window.log_event).toBe Logger.log
describe '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', -> it 'send a request to log event', ->
spyOn $, 'getWithPrefix' spyOn $, 'getWithPrefix'
Logger.log 'example', 'data' Logger.log 'example', 'data'
......
class @Logger class @Logger
# events we want sent to Segment.io for tracking
SEGMENT_IO_WHITELIST = ["seq_goto", "seq_next", "seq_prev"]
@log: (event_type, data) -> @log: (event_type, data) ->
if event_type in SEGMENT_IO_WHITELIST
# Segment.io event tracking
analytics.track event_type, data
$.getWithPrefix '/event', $.getWithPrefix '/event',
event_type: event_type event_type: event_type
event: JSON.stringify(data) event: JSON.stringify(data)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
!function(e,t){"use strict";var n=t.prototype.trim,r=t.prototype.trimRight,i=t.prototype.trimLeft,s=function(e){return e*1||0},o=function(e,t){if(t<1)return"";var n="";while(t>0)t&1&&(n+=e),t>>=1,e+=e;return n},u=[].slice,a=function(e){return e==null?"\\s":e.source?e.source:"["+p.escapeRegExp(e)+"]"},f={lt:"<",gt:">",quot:'"',apos:"'",amp:"&"},l={};for(var c in f)l[f[c]]=c;var h=function(){function e(e){return Object.prototype.toString.call(e).slice(8,-1).toLowerCase()}var n=o,r=function(){return r.cache.hasOwnProperty(arguments[0])||(r.cache[arguments[0]]=r.parse(arguments[0])),r.format.call(null,r.cache[arguments[0]],arguments)};return r.format=function(r,i){var s=1,o=r.length,u="",a,f=[],l,c,p,d,v,m;for(l=0;l<o;l++){u=e(r[l]);if(u==="string")f.push(r[l]);else if(u==="array"){p=r[l];if(p[2]){a=i[s];for(c=0;c<p[2].length;c++){if(!a.hasOwnProperty(p[2][c]))throw new Error(h('[_.sprintf] property "%s" does not exist',p[2][c]));a=a[p[2][c]]}}else p[1]?a=i[p[1]]:a=i[s++];if(/[^s]/.test(p[8])&&e(a)!="number")throw new Error(h("[_.sprintf] expecting number but found %s",e(a)));switch(p[8]){case"b":a=a.toString(2);break;case"c":a=t.fromCharCode(a);break;case"d":a=parseInt(a,10);break;case"e":a=p[7]?a.toExponential(p[7]):a.toExponential();break;case"f":a=p[7]?parseFloat(a).toFixed(p[7]):parseFloat(a);break;case"o":a=a.toString(8);break;case"s":a=(a=t(a))&&p[7]?a.substring(0,p[7]):a;break;case"u":a=Math.abs(a);break;case"x":a=a.toString(16);break;case"X":a=a.toString(16).toUpperCase()}a=/[def]/.test(p[8])&&p[3]&&a>=0?"+"+a:a,v=p[4]?p[4]=="0"?"0":p[4].charAt(1):" ",m=p[6]-t(a).length,d=p[6]?n(v,m):"",f.push(p[5]?a+d:d+a)}}return f.join("")},r.cache={},r.parse=function(e){var t=e,n=[],r=[],i=0;while(t){if((n=/^[^\x25]+/.exec(t))!==null)r.push(n[0]);else if((n=/^\x25{2}/.exec(t))!==null)r.push("%");else{if((n=/^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(t))===null)throw new Error("[_.sprintf] huh?");if(n[2]){i|=1;var s=[],o=n[2],u=[];if((u=/^([a-z_][a-z_\d]*)/i.exec(o))===null)throw new Error("[_.sprintf] huh?");s.push(u[1]);while((o=o.substring(u[0].length))!=="")if((u=/^\.([a-z_][a-z_\d]*)/i.exec(o))!==null)s.push(u[1]);else{if((u=/^\[(\d+)\]/.exec(o))===null)throw new Error("[_.sprintf] huh?");s.push(u[1])}n[2]=s}else i|=2;if(i===3)throw new Error("[_.sprintf] mixing positional and named placeholders is not (yet) supported");r.push(n)}t=t.substring(n[0].length)}return r},r}(),p={VERSION:"2.3.0",isBlank:function(e){return e==null&&(e=""),/^\s*$/.test(e)},stripTags:function(e){return e==null?"":t(e).replace(/<\/?[^>]+>/g,"")},capitalize:function(e){return e=e==null?"":t(e),e.charAt(0).toUpperCase()+e.slice(1)},chop:function(e,n){return e==null?[]:(e=t(e),n=~~n,n>0?e.match(new RegExp(".{1,"+n+"}","g")):[e])},clean:function(e){return p.strip(e).replace(/\s+/g," ")},count:function(e,n){return e==null||n==null?0:t(e).split(n).length-1},chars:function(e){return e==null?[]:t(e).split("")},swapCase:function(e){return e==null?"":t(e).replace(/\S/g,function(e){return e===e.toUpperCase()?e.toLowerCase():e.toUpperCase()})},escapeHTML:function(e){return e==null?"":t(e).replace(/[&<>"']/g,function(e){return"&"+l[e]+";"})},unescapeHTML:function(e){return e==null?"":t(e).replace(/\&([^;]+);/g,function(e,n){var r;return n in f?f[n]:(r=n.match(/^#x([\da-fA-F]+)$/))?t.fromCharCode(parseInt(r[1],16)):(r=n.match(/^#(\d+)$/))?t.fromCharCode(~~r[1]):e})},escapeRegExp:function(e){return e==null?"":t(e).replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")},splice:function(e,t,n,r){var i=p.chars(e);return i.splice(~~t,~~n,r),i.join("")},insert:function(e,t,n){return p.splice(e,t,0,n)},include:function(e,n){return n===""?!0:e==null?!1:t(e).indexOf(n)!==-1},join:function(){var e=u.call(arguments),t=e.shift();return t==null&&(t=""),e.join(t)},lines:function(e){return e==null?[]:t(e).split("\n")},reverse:function(e){return p.chars(e).reverse().join("")},startsWith:function(e,n){return n===""?!0:e==null||n==null?!1:(e=t(e),n=t(n),e.length>=n.length&&e.slice(0,n.length)===n)},endsWith:function(e,n){return n===""?!0:e==null||n==null?!1:(e=t(e),n=t(n),e.length>=n.length&&e.slice(e.length-n.length)===n)},succ:function(e){return e==null?"":(e=t(e),e.slice(0,-1)+t.fromCharCode(e.charCodeAt(e.length-1)+1))},titleize:function(e){return e==null?"":t(e).replace(/(?:^|\s)\S/g,function(e){return e.toUpperCase()})},camelize:function(e){return p.trim(e).replace(/[-_\s]+(.)?/g,function(e,t){return t.toUpperCase()})},underscored:function(e){return p.trim(e).replace(/([a-z\d])([A-Z]+)/g,"$1_$2").replace(/[-\s]+/g,"_").toLowerCase()},dasherize:function(e){return p.trim(e).replace(/([A-Z])/g,"-$1").replace(/[-_\s]+/g,"-").toLowerCase()},classify:function(e){return p.titleize(t(e).replace(/_/g," ")).replace(/\s/g,"")},humanize:function(e){return p.capitalize(p.underscored(e).replace(/_id$/,"").replace(/_/g," "))},trim:function(e,r){return e==null?"":!r&&n?n.call(e):(r=a(r),t(e).replace(new RegExp("^"+r+"+|"+r+"+$","g"),""))},ltrim:function(e,n){return e==null?"":!n&&i?i.call(e):(n=a(n),t(e).replace(new RegExp("^"+n+"+"),""))},rtrim:function(e,n){return e==null?"":!n&&r?r.call(e):(n=a(n),t(e).replace(new RegExp(n+"+$"),""))},truncate:function(e,n,r){return e==null?"":(e=t(e),r=r||"...",n=~~n,e.length>n?e.slice(0,n)+r:e)},prune:function(e,n,r){if(e==null)return"";e=t(e),n=~~n,r=r!=null?t(r):"...";if(e.length<=n)return e;var i=function(e){return e.toUpperCase()!==e.toLowerCase()?"A":" "},s=e.slice(0,n+1).replace(/.(?=\W*\w*$)/g,i);return s.slice(s.length-2).match(/\w\w/)?s=s.replace(/\s*\S+$/,""):s=p.rtrim(s.slice(0,s.length-1)),(s+r).length>e.length?e:e.slice(0,s.length)+r},words:function(e,t){return p.isBlank(e)?[]:p.trim(e,t).split(t||/\s+/)},pad:function(e,n,r,i){e=e==null?"":t(e),n=~~n;var s=0;r?r.length>1&&(r=r.charAt(0)):r=" ";switch(i){case"right":return s=n-e.length,e+o(r,s);case"both":return s=n-e.length,o(r,Math.ceil(s/2))+e+o(r,Math.floor(s/2));default:return s=n-e.length,o(r,s)+e}},lpad:function(e,t,n){return p.pad(e,t,n)},rpad:function(e,t,n){return p.pad(e,t,n,"right")},lrpad:function(e,t,n){return p.pad(e,t,n,"both")},sprintf:h,vsprintf:function(e,t){return t.unshift(e),h.apply(null,t)},toNumber:function(e,n){if(e==null||e=="")return 0;e=t(e);var r=s(s(e).toFixed(~~n));return r===0&&!e.match(/^0+$/)?Number.NaN:r},numberFormat:function(e,t,n,r){if(isNaN(e)||e==null)return"";e=e.toFixed(~~t),r=r||",";var i=e.split("."),s=i[0],o=i[1]?(n||".")+i[1]:"";return s.replace(/(\d)(?=(?:\d{3})+$)/g,"$1"+r)+o},strRight:function(e,n){if(e==null)return"";e=t(e),n=n!=null?t(n):n;var r=n?e.indexOf(n):-1;return~r?e.slice(r+n.length,e.length):e},strRightBack:function(e,n){if(e==null)return"";e=t(e),n=n!=null?t(n):n;var r=n?e.lastIndexOf(n):-1;return~r?e.slice(r+n.length,e.length):e},strLeft:function(e,n){if(e==null)return"";e=t(e),n=n!=null?t(n):n;var r=n?e.indexOf(n):-1;return~r?e.slice(0,r):e},strLeftBack:function(e,t){if(e==null)return"";e+="",t=t!=null?""+t:t;var n=e.lastIndexOf(t);return~n?e.slice(0,n):e},toSentence:function(e,t,n,r){t=t||", ",n=n||" and ";var i=e.slice(),s=i.pop();return e.length>2&&r&&(n=p.rtrim(t)+n),i.length?i.join(t)+n+s:s},toSentenceSerial:function(){var e=u.call(arguments);return e[3]=!0,p.toSentence.apply(p,e)},slugify:function(e){if(e==null)return"";var n="ąàáäâãåæćęèéëêìíïîłńòóöôõøùúüûñçżź",r="aaaaaaaaceeeeeiiiilnoooooouuuunczz",i=new RegExp(a(n),"g");return e=t(e).toLowerCase().replace(i,function(e){var t=n.indexOf(e);return r.charAt(t)||"-"}),p.dasherize(e.replace(/[^\w\s-]/g,""))},surround:function(e,t){return[t,e,t].join("")},quote:function(e){return p.surround(e,'"')},exports:function(){var e={};for(var t in this){if(!this.hasOwnProperty(t)||t.match(/^(?:include|contains|reverse)$/))continue;e[t]=this[t]}return e},repeat:function(e,n,r){if(e==null)return"";n=~~n;if(r==null)return o(t(e),n);for(var i=[];n>0;i[--n]=e);return i.join(r)},levenshtein:function(e,n){if(e==null&&n==null)return 0;if(e==null)return t(n).length;if(n==null)return t(e).length;e=t(e),n=t(n);var r=[],i,s;for(var o=0;o<=n.length;o++)for(var u=0;u<=e.length;u++)o&&u?e.charAt(u-1)===n.charAt(o-1)?s=i:s=Math.min(r[u],r[u-1],i)+1:s=o+u,i=r[u],r[u]=s;return r.pop()}};p.strip=p.trim,p.lstrip=p.ltrim,p.rstrip=p.rtrim,p.center=p.lrpad,p.rjust=p.lpad,p.ljust=p.rpad,p.contains=p.include,p.q=p.quote,typeof exports!="undefined"?(typeof module!="undefined"&&module.exports&&(module.exports=p),exports._s=p):typeof define=="function"&&define.amd?define("underscore.string",[],function(){return p}):(e._=e._||{},e._.string=e._.str=p)}(this,String);
\ No newline at end of file
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=default"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=default"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.timeago.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.timeago.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/sinon-1.7.1.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/analytics.js"></script>
<script type="text/javascript"> <script type="text/javascript">
AjaxPrefix.addAjaxPrefix(jQuery, function() { AjaxPrefix.addAjaxPrefix(jQuery, function() {
return ""; return "";
......
...@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use: ...@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use:
rake -T rake -T
### Troubleshooting
#### Reference Error: XModule is not defined (javascript)
This means that the javascript defining an xmodule hasn't loaded correctly. There are a number
of different things that could be causing this:
1. See `Error: watch EMFILE`
#### Error: watch EMFILE (coffee)
When running a development server, we also start a watcher process alongside to recompile coffeescript
and sass as changes are made. On Mac OSX systems, the coffee watcher process takes more file handles
than are allowed by default. This will result in `EMFILE` errors when coffeescript is running, and
will prevent javascript from compiling, leading to the error 'XModule is not defined'
To work around this issue, we use `Process::setrlimit` to set the number of allowed open files.
Coffee watches both directories and files, so you will need to set this fairly high (anecdotally,
8000 seems to do the trick on OSX 10.7.5, 10.8.3, and 10.8.4)
## Running Tests ## Running Tests
See `testing.md` for instructions on running the test suite. See `testing.md` for instructions on running the test suite.
......
...@@ -23,8 +23,11 @@ be specified for this tag:: ...@@ -23,8 +23,11 @@ be specified for this tag::
sources - location id of required modules, separated by ';' sources - location id of required modules, separated by ';'
[message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module. [message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module.
[submitted] - map to `is_submitted` module method.
(pressing RESET button makes this function to return False.)
[completed] - map to `is_completed` module method [correct] - map to `is_correct` module method
[attempted] - map to `is_attempted` module method [attempted] - map to `is_attempted` module method
[poll_answer] - map to `poll_answer` module attribute [poll_answer] - map to `poll_answer` module attribute
[voted] - map to `voted` module attribute [voted] - map to `voted` module attribute
...@@ -53,7 +56,7 @@ Examples of conditional depends on poll ...@@ -53,7 +56,7 @@ Examples of conditional depends on poll
</conditional> </conditional>
Examples of conditional depends on poll (use <show> tag) Examples of conditional depends on poll (use <show> tag)
------------------------------------------- --------------------------------------------------------
.. code-block:: xml .. code-block:: xml
......
...@@ -420,6 +420,6 @@ Draggables can be reused ...@@ -420,6 +420,6 @@ Draggables can be reused
.. literalinclude:: drag-n-drop-demo2.xml .. literalinclude:: drag-n-drop-demo2.xml
Examples of targets on draggables Examples of targets on draggables
------------------------ ---------------------------------
.. literalinclude:: drag-n-drop-demo3.xml .. literalinclude:: drag-n-drop-demo3.xml
...@@ -362,7 +362,7 @@ that has to be updated on a parameter's change, then one can define ...@@ -362,7 +362,7 @@ that has to be updated on a parameter's change, then one can define
a special function to handle this. The "output" of such a function must be a special function to handle this. The "output" of such a function must be
set to "none", and the JavaScript code inside this function must update the set to "none", and the JavaScript code inside this function must update the
MathJax element by itself. Before exiting, MathJax typeset function should MathJax element by itself. Before exiting, MathJax typeset function should
be called so that the new text will be re-rendered by MathJax. For example, be called so that the new text will be re-rendered by MathJax. For example::
<render> <render>
... ...
......
...@@ -19,11 +19,11 @@ This is a partial list of features, to be revised as we go along: ...@@ -19,11 +19,11 @@ This is a partial list of features, to be revised as we go along:
An example of a problem:: An example of a problem::
<symbolicresponse expect="a_b^c + b_x__d" size="30"> <symbolicresponse expect="a_b^c + b_x__d" size="30">
<textline math="1" <textline math="1"
preprocessorClassName="SymbolicMathjaxPreprocessor" preprocessorClassName="SymbolicMathjaxPreprocessor"
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/> preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
</symbolicresponse> </symbolicresponse>
It's a bit of a pain to enter that. It's a bit of a pain to enter that.
......
...@@ -28,6 +28,7 @@ Specific Problem Types ...@@ -28,6 +28,7 @@ Specific Problem Types
course_data_formats/conditional_module/conditional_module.rst course_data_formats/conditional_module/conditional_module.rst
course_data_formats/word_cloud/word_cloud.rst course_data_formats/word_cloud/word_cloud.rst
course_data_formats/custom_response.rst course_data_formats/custom_response.rst
course_data_formats/symbolic_response.rst
Internal Data Formats Internal Data Formats
......
*******************************************
Calc
*******************************************
.. automodule:: calc
:members:
:show-inheritance:
...@@ -8,14 +8,6 @@ Contents: ...@@ -8,14 +8,6 @@ Contents:
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
chem.rst
Calc
====
.. automodule:: capa.calc
:members:
:show-inheritance:
Capa_problem Capa_problem
============ ============
......
******************************************* *******************************************
Chem module Chemistry modules
******************************************* *******************************************
.. module:: chem .. module:: chem
...@@ -7,7 +7,7 @@ Chem module ...@@ -7,7 +7,7 @@ Chem module
Miller Miller
====== ======
.. automodule:: capa.chem.miller .. automodule:: chem.miller
:members: :members:
:show-inheritance: :show-inheritance:
...@@ -47,14 +47,14 @@ Documentation from **crystallography.js**:: ...@@ -47,14 +47,14 @@ Documentation from **crystallography.js**::
Chemcalc Chemcalc
======== ========
.. automodule:: capa.chem.chemcalc .. automodule:: chem.chemcalc
:members: :members:
:show-inheritance: :show-inheritance:
Chemtools Chemtools
========= =========
.. automodule:: capa.chem.chemtools .. automodule:: chem.chemtools
:members: :members:
:show-inheritance: :show-inheritance:
...@@ -62,7 +62,7 @@ Chemtools ...@@ -62,7 +62,7 @@ Chemtools
Tests Tests
===== =====
.. automodule:: capa.chem.tests .. automodule:: chem.tests
:members: :members:
:show-inheritance: :show-inheritance:
......
...@@ -4,86 +4,3 @@ CMS module ...@@ -4,86 +4,3 @@ CMS module
.. module:: cms .. module:: cms
Auth
====
.. automodule:: auth
:members:
:show-inheritance:
Authz
-----
.. automodule:: auth.authz
:members:
:show-inheritance:
Content store
=============
.. .. automodule:: contentstore
.. :members:
.. :show-inheritance:
.. Utils
.. -----
.. .. automodule:: contentstore.untils
.. :members:
.. :show-inheritance:
.. Views
.. -----
.. .. automodule:: contentstore.views
.. :members:
.. :show-inheritance:
.. Management
.. ----------
.. .. automodule:: contentstore.management
.. :members:
.. :show-inheritance:
.. Tests
.. -----
.. .. automodule:: contentstore.tests
.. :members:
.. :show-inheritance:
Github sync
===========
.. automodule:: github_sync
:members:
:show-inheritance:
Exceptions
----------
.. automodule:: github_sync.exceptions
:members:
:show-inheritance:
Views
-----
.. automodule:: github_sync.views
:members:
:show-inheritance:
Management
----------
.. automodule:: github_sync.management
:members:
:show-inheritance:
Tests
-----
.. .. automodule:: github_sync.tests
.. :members:
.. :show-inheritance:
\ No newline at end of file
...@@ -6,4 +6,9 @@ Contents: ...@@ -6,4 +6,9 @@ Contents:
:maxdepth: 2 :maxdepth: 2
xmodule.rst xmodule.rst
capa.rst capa.rst
\ No newline at end of file chem.rst
sandbox-packages.rst
symmath.rst
calc.rst
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #pylint: disable=C0103
# MITx documentation build configuration file, created by #pylint: disable=W0622
# sphinx-quickstart on Fri Nov 2 15:43:00 2012. #pylint: disable=W0212
# #pylint: disable=W0613
# This file is execfile()d with the current directory set to its containing dir. """ EdX documentation build configuration file, created by
# sphinx-quickstart on Fri Nov 2 15:43:00 2012.
# Note that not all possible configuration values are present in this
# autogenerated file. This file is execfile()d with the current directory set to its containing dir.
#
# All configuration values have a default; values that are commented out Note that not all possible configuration values are present in this
# serve to show the default. autogenerated file.
import sys, os All configuration values have a default; values that are commented out
serve to show the default."""
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('../..')) # mitx folder sys.path.insert(0, os.path.abspath('../..')) # mitx folder
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'capa')) # capa module
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'xmodule')) # xmodule
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'lms', 'djangoapps')) # lms djangoapps
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'cms', 'djangoapps')) # cms djangoapps
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'djangoapps')) # common djangoapps
# django configuration - careful here # django configuration - careful here
import os os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.test'
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
...@@ -36,7 +34,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev' ...@@ -36,7 +34,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode'] extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage',
'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
...@@ -51,17 +51,17 @@ source_suffix = '.rst' ...@@ -51,17 +51,17 @@ source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'MITx' project = u'EdX Dev Data'
copyright = u'2012, MITx team' copyright = u'2012-13, EdX team'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '1.0' version = '0.2'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '1.0' release = '0.2'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
...@@ -75,7 +75,7 @@ release = '1.0' ...@@ -75,7 +75,7 @@ release = '1.0'
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
exclude_patterns = [] exclude_patterns = ['build']
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None #default_role = None
...@@ -175,27 +175,27 @@ html_static_path = ['_static'] ...@@ -175,27 +175,27 @@ html_static_path = ['_static']
#html_file_suffix = None #html_file_suffix = None
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'MITxdoc' htmlhelp_basename = 'edXDocs'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper', #'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt', #'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#'preamble': '', #'preamble': '',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'MITx.tex', u'MITx Documentation', ('index', 'edXDocs.tex', u'EdX Dev Data Documentation',
u'MITx team', 'manual'), u'EdX Team', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
...@@ -224,8 +224,8 @@ latex_documents = [ ...@@ -224,8 +224,8 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'mitx', u'MITx Documentation', ('index', 'edxdocs', u'EdX Dev Data Documentation',
[u'MITx team'], 1) [u'EdX Team'], 1)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
...@@ -238,9 +238,9 @@ man_pages = [ ...@@ -238,9 +238,9 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'MITx', u'MITx Documentation', ('index', 'EdXDocs', u'EdX Dev Data Documentation',
u'MITx team', 'MITx', 'One line description of project.', u'EdX Team', 'EdXDocs', 'One line description of project.',
'Miscellaneous'), 'Miscellaneous'),
] ]
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
...@@ -265,8 +265,12 @@ from django.utils.encoding import force_unicode ...@@ -265,8 +265,12 @@ from django.utils.encoding import force_unicode
def process_docstring(app, what, name, obj, options, lines): def process_docstring(app, what, name, obj, options, lines):
"""Autodoc django models"""
# This causes import errors if left outside the function # This causes import errors if left outside the function
from django.db import models from django.db import models
# If you want extract docs from django forms:
# from django import forms # from django import forms
# from django.forms.models import BaseInlineFormSet # from django.forms.models import BaseInlineFormSet
...@@ -326,5 +330,6 @@ def process_docstring(app, what, name, obj, options, lines): ...@@ -326,5 +330,6 @@ def process_docstring(app, what, name, obj, options, lines):
def setup(app): def setup(app):
# Register the docstring processor with sphinx """Setup docsting processors"""
#Register the docstring processor with sphinx
app.connect('autodoc-process-docstring', process_docstring) app.connect('autodoc-process-docstring', process_docstring)
.. MITx documentation master file, created by .. EdX Dev documentation master file, created by
sphinx-quickstart on Fri Nov 2 15:43:00 2012. sphinx-quickstart on Fri Nov 2 15:43:00 2012.
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
Welcome to MITx's documentation! Welcome to EdX's Dev documentation!
================================ ===================================
Contents: Contents:
......
******************************************* *******************************************
What the pieces are? Overview
******************************************* *******************************************
What
====
... This is EdX Dev documentation, mainly extracted from docstrings.
Autogenerated by Sphinx from python code.
Soon support for JS will be impemented.
How
===
...
Who
===
...
\ No newline at end of file
*******************************************
Sandbox-packages
*******************************************
.. module:: sandbox-packages
Loncapa
=======
.. automodule:: loncapa.loncapa_check
:members:
:show-inheritance:
\ No newline at end of file
*******************************************
Symmath
*******************************************
.. module:: symmath
Formula
=======
.. automodule:: symmath.formula
:members:
:show-inheritance:
Symmath check
=============
.. automodule:: symmath.symmath_check
:members:
:show-inheritance:
Symmath tests
=============
.. automodule:: symmath.test_formula
:members:
:show-inheritance:
.. automodule:: symmath.test_symmath_check
:members:
:show-inheritance:
\ No newline at end of file
...@@ -144,13 +144,6 @@ Templates ...@@ -144,13 +144,6 @@ Templates
:members: :members:
:show-inheritance: :show-inheritance:
Time parse
==========
.. automodule:: xmodule.timeparse
:members:
:show-inheritance:
Vertical Vertical
======== ========
......
...@@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student ...@@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student
from certificates.models import CertificateStatuses as status from certificates.models import CertificateStatuses as status
from certificates.models import CertificateWhitelist from certificates.models import CertificateWhitelist
from mitxmako.middleware import MakoMiddleware
from courseware import grades, courses from courseware import grades, courses
from django.test.client import RequestFactory from django.test.client import RequestFactory
from capa.xqueue_interface import XQueueInterface from capa.xqueue_interface import XQueueInterface
...@@ -51,6 +52,14 @@ class XQueueCertInterface(object): ...@@ -51,6 +52,14 @@ class XQueueCertInterface(object):
""" """
def __init__(self, request=None): def __init__(self, request=None):
# MakoMiddleware Note:
# Line below has the side-effect of writing to a module level lookup
# table that will allow problems to render themselves. If this is not
# present, problems that a student hasn't seen will error when loading,
# causing the grading system to under-count the possible score and
# inflate their grade. This dependency is bad and was probably recently
# introduced. This is the bandage until we can trace the root cause.
m = MakoMiddleware()
# Get basic auth (username/password) for # Get basic auth (username/password) for
# xqueue connection if it's in the settings # xqueue connection if it's in the settings
...@@ -161,6 +170,10 @@ class XQueueCertInterface(object): ...@@ -161,6 +170,10 @@ class XQueueCertInterface(object):
cert, created = GeneratedCertificate.objects.get_or_create( cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id) user=student, course_id=course_id)
# Needed
self.request.user = student
self.request.session = {}
grade = grades.grade(student, self.request, course) grade = grades.grade(student, self.request, course)
is_whitelisted = self.whitelist.filter( is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists() user=student, course_id=course_id, whitelist=True).exists()
...@@ -211,5 +224,5 @@ class XQueueCertInterface(object): ...@@ -211,5 +224,5 @@ class XQueueCertInterface(object):
(error, msg) = self.xqueue_interface.send_to_queue( (error, msg) = self.xqueue_interface.send_to_queue(
header=xheader, body=json.dumps(contents)) header=xheader, body=json.dumps(contents))
if error: if error:
logger.critical('Unable to add a request to the queue') logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
raise Exception('Unable to send queue message') raise Exception('Unable to send queue message')
#pylint: disable=C0111 # pylint: disable=C0111
#pylint: disable=W0621 # pylint: disable=W0621
from __future__ import absolute_import from __future__ import absolute_import
......
...@@ -91,7 +91,7 @@ def click_on_section(step, section): ...@@ -91,7 +91,7 @@ def click_on_section(step, section):
@step(u'I click on subsection "([^"]*)"$') @step(u'I click on subsection "([^"]*)"$')
def click_on_subsection(step, subsection): def click_on_subsection(step, subsection):
subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a' subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a'
world.css_find(subsection_css)[int(subsection) - 1].click() world.css_click(subsection_css, index=(int(subsection) - 1))
@step(u'I click on sequence "([^"]*)"$') @step(u'I click on sequence "([^"]*)"$')
......
...@@ -112,12 +112,12 @@ def assert_problem_has_answer(step, problem_type, answer_class): ...@@ -112,12 +112,12 @@ def assert_problem_has_answer(step, problem_type, answer_class):
@step(u'I reset the problem') @step(u'I reset the problem')
def reset_problem(step): def reset_problem(_step):
world.css_click('input.reset') world.css_click('input.reset')
@step(u'I press the button with the label "([^"]*)"$') @step(u'I press the button with the label "([^"]*)"$')
def press_the_button_with_label(step, buttonname): def press_the_button_with_label(_step, buttonname):
button_css = 'button span.show-label' button_css = 'button span.show-label'
elem = world.css_find(button_css).first elem = world.css_find(button_css).first
assert_equal(elem.text, buttonname) assert_equal(elem.text, buttonname)
...@@ -125,7 +125,7 @@ def press_the_button_with_label(step, buttonname): ...@@ -125,7 +125,7 @@ def press_the_button_with_label(step, buttonname):
@step(u'The "([^"]*)" button does( not)? appear') @step(u'The "([^"]*)" button does( not)? appear')
def action_button_present(step, buttonname, doesnt_appear): def action_button_present(_step, buttonname, doesnt_appear):
button_css = 'section.action input[value*="%s"]' % buttonname button_css = 'section.action input[value*="%s"]' % buttonname
if doesnt_appear: if doesnt_appear:
assert world.is_css_not_present(button_css) assert world.is_css_not_present(button_css)
......
#pylint: disable=C0111 # pylint: disable=C0111
#pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step from lettuce import world, step
from lettuce.django import django_url from lettuce.django import django_url
...@@ -7,7 +7,7 @@ from common import TEST_COURSE_ORG, TEST_COURSE_NAME ...@@ -7,7 +7,7 @@ from common import TEST_COURSE_ORG, TEST_COURSE_NAME
@step('I register for the course "([^"]*)"$') @step('I register for the course "([^"]*)"$')
def i_register_for_the_course(step, course): def i_register_for_the_course(_step, course):
cleaned_name = TEST_COURSE_NAME.replace(' ', '_') cleaned_name = TEST_COURSE_NAME.replace(' ', '_')
url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name)) url = django_url('courses/%s/%s/%s/about' % (TEST_COURSE_ORG, course, cleaned_name))
world.browser.visit(url) world.browser.visit(url)
...@@ -20,13 +20,13 @@ def i_register_for_the_course(step, course): ...@@ -20,13 +20,13 @@ def i_register_for_the_course(step, course):
@step(u'I should see an empty dashboard message') @step(u'I should see an empty dashboard message')
def i_should_see_empty_dashboard(step): def i_should_see_empty_dashboard(_step):
empty_dash_css = 'section.empty-dashboard-message' empty_dash_css = 'section.empty-dashboard-message'
assert world.is_css_present(empty_dash_css) assert world.is_css_present(empty_dash_css)
@step(u'I should( NOT)? see the course numbered "([^"]*)" in my dashboard$') @step(u'I should( NOT)? see the course numbered "([^"]*)" in my dashboard$')
def i_should_see_that_course_in_my_dashboard(step, doesnt_appear, course): def i_should_see_that_course_in_my_dashboard(_step, doesnt_appear, course):
course_link_css = 'section.my-courses a[href*="%s"]' % course course_link_css = 'section.my-courses a[href*="%s"]' % course
if doesnt_appear: if doesnt_appear:
assert world.is_css_not_present(course_link_css) assert world.is_css_not_present(course_link_css)
...@@ -35,7 +35,7 @@ def i_should_see_that_course_in_my_dashboard(step, doesnt_appear, course): ...@@ -35,7 +35,7 @@ def i_should_see_that_course_in_my_dashboard(step, doesnt_appear, course):
@step(u'I unregister for the course numbered "([^"]*)"') @step(u'I unregister for the course numbered "([^"]*)"')
def i_unregister_for_that_course(step, course): def i_unregister_for_that_course(_step, course):
unregister_css = 'section.info a[href*="#unenroll-modal"][data-course-number*="%s"]' % course unregister_css = 'section.info a[href*="#unenroll-modal"][data-course-number*="%s"]' % course
world.css_click(unregister_css) world.css_click(unregister_css)
button_css = 'section#unenroll-modal input[value="Unregister"]' button_css = 'section#unenroll-modal input[value="Unregister"]'
......
...@@ -8,12 +8,12 @@ from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_ ...@@ -8,12 +8,12 @@ from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_
@step('when I view the video it has autoplay enabled') @step('when I view the video it has autoplay enabled')
def does_autoplay(step): def does_autoplay(_step):
assert(world.css_find('.video')[0]['data-autoplay'] == 'True') assert(world.css_find('.video')[0]['data-autoplay'] == 'True')
@step('the course has a Video component') @step('the course has a Video component')
def view_video(step): def view_video(_step):
coursename = TEST_COURSE_NAME.replace(' ', '_') coursename = TEST_COURSE_NAME.replace(' ', '_')
i_am_registered_for_the_course(step, coursename) i_am_registered_for_the_course(step, coursename)
......
...@@ -4,9 +4,9 @@ WE'RE USING MIGRATIONS! ...@@ -4,9 +4,9 @@ WE'RE USING MIGRATIONS!
If you make changes to this model, be sure to create an appropriate migration If you make changes to this model, be sure to create an appropriate migration
file and check it in at the same time as your model changes. To do that, file and check it in at the same time as your model changes. To do that,
1. Go to the mitx dir 1. Go to the edx-platform dir
2. ./manage.py schemamigration courseware --auto description_of_your_change 2. ./manage.py schemamigration courseware --auto description_of_your_change
3. Add the migration file created in mitx/courseware/migrations/ 3. Add the migration file created in edx-platform/lms/djangoapps/courseware/migrations/
ASSUMPTIONS: modules have unique IDs, even across different module_types ASSUMPTIONS: modules have unique IDs, even across different module_types
...@@ -17,6 +17,7 @@ from django.db import models ...@@ -17,6 +17,7 @@ from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
class StudentModule(models.Model): class StudentModule(models.Model):
""" """
Keeps student state for a particular module in a particular course. Keeps student state for a particular module in a particular course.
......
...@@ -121,7 +121,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_ ...@@ -121,7 +121,7 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_
def get_module(user, request, location, model_data_cache, course_id, def get_module(user, request, location, model_data_cache, course_id,
position=None, not_found_ok = False, wrap_xmodule_display=True, position=None, not_found_ok=False, wrap_xmodule_display=True,
grade_bucket_type=None, depth=0): grade_bucket_type=None, depth=0):
""" """
Get an instance of the xmodule class identified by location, Get an instance of the xmodule class identified by location,
...@@ -161,16 +161,49 @@ def get_module(user, request, location, model_data_cache, course_id, ...@@ -161,16 +161,49 @@ def get_module(user, request, location, model_data_cache, course_id,
return None return None
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, def get_xqueue_callback_url_prefix(request):
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
""" """
Actually implement get_module. See docstring there for details. Calculates default prefix based on request, but allows override via settings
This is separated from get_module_for_descriptor so that it can be called
by the LMS before submitting background tasks to run. The xqueue callbacks
should go back to the LMS, not to the worker.
""" """
prefix = '{proto}://{host}'.format(
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http'),
host=request.get_host()
)
return settings.XQUEUE_INTERFACE.get('callback_url', prefix)
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
"""
Implements get_module, extracting out the request-specific functionality.
See get_module() docstring for further details.
"""
# allow course staff to masquerade as student # allow course staff to masquerade as student
if has_access(user, descriptor, 'staff', course_id): if has_access(user, descriptor, 'staff', course_id):
setup_masquerade(request, True) setup_masquerade(request, True)
track_function = make_track_function(request)
xqueue_callback_url_prefix = get_xqueue_callback_url_prefix(request)
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, xqueue_callback_url_prefix,
position, wrap_xmodule_display, grade_bucket_type)
def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, xqueue_callback_url_prefix,
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
"""
Actually implement get_module, without requiring a request.
See get_module() docstring for further details.
"""
# Short circuit--if the user shouldn't have access, bail without doing any work # Short circuit--if the user shouldn't have access, bail without doing any work
if not has_access(user, descriptor, 'load', course_id): if not has_access(user, descriptor, 'load', course_id):
return None return None
...@@ -186,19 +219,13 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -186,19 +219,13 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
def make_xqueue_callback(dispatch='score_update'): def make_xqueue_callback(dispatch='score_update'):
# Fully qualified callback URL for external queueing system # Fully qualified callback URL for external queueing system
xqueue_callback_url = '{proto}://{host}'.format( relative_xqueue_callback_url = reverse('xqueue_callback',
host=request.get_host(), kwargs=dict(course_id=course_id,
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') userid=str(user.id),
) id=descriptor.location.url(),
xqueue_callback_url = settings.XQUEUE_INTERFACE.get('callback_url',xqueue_callback_url) # allow override dispatch=dispatch),
)
xqueue_callback_url += reverse('xqueue_callback', return xqueue_callback_url_prefix + relative_xqueue_callback_url
kwargs=dict(course_id=course_id,
userid=str(user.id),
id=descriptor.location.url(),
dispatch=dispatch),
)
return xqueue_callback_url
# Default queuename is course-specific and is derived from the course that # Default queuename is course-specific and is derived from the course that
# contains the current module. # contains the current module.
...@@ -211,20 +238,20 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -211,20 +238,20 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS 'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
} }
#This is a hacky way to pass settings to the combined open ended xmodule # This is a hacky way to pass settings to the combined open ended xmodule
#It needs an S3 interface to upload images to S3 # It needs an S3 interface to upload images to S3
#It needs the open ended grading interface in order to get peer grading to be done # It needs the open ended grading interface in order to get peer grading to be done
#this first checks to see if the descriptor is the correct one, and only sends settings if it is # this first checks to see if the descriptor is the correct one, and only sends settings if it is
#Get descriptor metadata fields indicating needs for various settings # Get descriptor metadata fields indicating needs for various settings
needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False) needs_open_ended_interface = getattr(descriptor, "needs_open_ended_interface", False)
needs_s3_interface = getattr(descriptor, "needs_s3_interface", False) needs_s3_interface = getattr(descriptor, "needs_s3_interface", False)
#Initialize interfaces to None # Initialize interfaces to None
open_ended_grading_interface = None open_ended_grading_interface = None
s3_interface = None s3_interface = None
#Create interfaces if needed # Create interfaces if needed
if needs_open_ended_interface: if needs_open_ended_interface:
open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE open_ended_grading_interface = settings.OPEN_ENDED_GRADING_INTERFACE
open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING open_ended_grading_interface['mock_peer_grading'] = settings.MOCK_PEER_GRADING
...@@ -238,10 +265,15 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -238,10 +265,15 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
def inner_get_module(descriptor): def inner_get_module(descriptor):
""" """
Delegate to get_module. It does an access check, so may return None Delegate to get_module_for_descriptor_internal() with all values except `descriptor` set.
Because it does an access check, it may return None.
""" """
return get_module_for_descriptor(user, request, descriptor, # TODO: fix this so that make_xqueue_callback uses the descriptor passed into
model_data_cache, course_id, position) # inner_get_module, not the parent's callback. Add it as an argument....
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, make_xqueue_callback,
position, wrap_xmodule_display, grade_bucket_type)
def xblock_model_data(descriptor): def xblock_model_data(descriptor):
return DbModel( return DbModel(
...@@ -266,7 +298,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -266,7 +298,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
student_module.max_grade = event.get('max_value') student_module.max_grade = event.get('max_value')
student_module.save() student_module.save()
#Bin score into range and increment stats # Bin score into range and increment stats
score_bucket = get_score_bucket(student_module.grade, student_module.max_grade) score_bucket = get_score_bucket(student_module.grade, student_module.max_grade)
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
...@@ -291,7 +323,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -291,7 +323,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
# TODO (cpennington): When modules are shared between courses, the static # TODO (cpennington): When modules are shared between courses, the static
# prefix is going to have to be specific to the module, not the directory # prefix is going to have to be specific to the module, not the directory
# that the xml was loaded from # that the xml was loaded from
system = ModuleSystem(track_function=make_track_function(request), system = ModuleSystem(track_function=track_function,
render_template=render_to_string, render_template=render_to_string,
ajax_url=ajax_url, ajax_url=ajax_url,
xqueue=xqueue, xqueue=xqueue,
...@@ -440,13 +472,13 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -440,13 +472,13 @@ def modx_dispatch(request, dispatch, location, course_id):
inputfiles = request.FILES.getlist(fileinput_id) inputfiles = request.FILES.getlist(fileinput_id)
if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT: if len(inputfiles) > settings.MAX_FILEUPLOADS_PER_INPUT:
too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' %\ too_many_files_msg = 'Submission aborted! Maximum %d files may be submitted at once' % \
settings.MAX_FILEUPLOADS_PER_INPUT settings.MAX_FILEUPLOADS_PER_INPUT
return HttpResponse(json.dumps({'success': too_many_files_msg})) return HttpResponse(json.dumps({'success': too_many_files_msg}))
for inputfile in inputfiles: for inputfile in inputfiles:
if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes if inputfile.size > settings.STUDENT_FILEUPLOAD_MAX_SIZE: # Bytes
file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' %\ file_too_big_msg = 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % \
(inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2)) (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2))
return HttpResponse(json.dumps({'success': file_too_big_msg})) return HttpResponse(json.dumps({'success': file_too_big_msg}))
p[fileinput_id] = inputfiles p[fileinput_id] = inputfiles
......
...@@ -13,7 +13,7 @@ from django.test.client import Client ...@@ -13,7 +13,7 @@ from django.test.client import Client
from student.tests.factories import UserFactory, CourseEnrollmentFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.tests import test_system from xmodule.tests import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -77,7 +77,7 @@ class BaseTestXmodule(ModuleStoreTestCase): ...@@ -77,7 +77,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
data=self.DATA data=self.DATA
) )
system = test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
model_data = {'location': self.item_descriptor.location} model_data = {'location': self.item_descriptor.location}
model_data.update(self.MODEL_DATA) model_data.update(self.MODEL_DATA)
......
...@@ -11,21 +11,22 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE ...@@ -11,21 +11,22 @@ from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
class ProgressTestCase(TestCase): class ProgressTestCase(TestCase):
def setUp(self): def setUp(self):
self.mockuser1 = MagicMock() self.mockuser1 = MagicMock()
self.mockuser0 = MagicMock() self.mockuser0 = MagicMock()
self.course = MagicMock() self.course = MagicMock()
self.mockuser1.is_authenticated.return_value = True self.mockuser1.is_authenticated.return_value = True
self.mockuser0.is_authenticated.return_value = False self.mockuser0.is_authenticated.return_value = False
self.course.id = 'edX/full/6.002_Spring_2012' self.course.id = 'edX/full/6.002_Spring_2012'
self.tab = {'name': 'same'} self.tab = {'name': 'same'}
self.active_page1 = 'progress' self.active_page1 = 'progress'
self.active_page0 = 'stagnation' self.active_page0 = 'stagnation'
def test_progress(self): def test_progress(self):
self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser0, self.course,
self.active_page0), []) self.active_page0), [])
...@@ -34,8 +35,8 @@ class ProgressTestCase(TestCase): ...@@ -34,8 +35,8 @@ class ProgressTestCase(TestCase):
self.active_page1)[0].name, 'same') self.active_page1)[0].name, 'same')
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
self.active_page1)[0].link, self.active_page1)[0].link,
reverse('progress', args = [self.course.id])) reverse('progress', args=[self.course.id]))
self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course, self.assertEqual(tabs._progress(self.tab, self.mockuser1, self.course,
self.active_page0)[0].is_active, False) self.active_page0)[0].is_active, False)
...@@ -63,15 +64,15 @@ class WikiTestCase(TestCase): ...@@ -63,15 +64,15 @@ class WikiTestCase(TestCase):
'same') 'same')
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page1)[0].link, self.course, self.active_page1)[0].link,
reverse('course_wiki', args=[self.course.id])) reverse('course_wiki', args=[self.course.id]))
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page1)[0].is_active, self.course, self.active_page1)[0].is_active,
True) True)
self.assertEqual(tabs._wiki(self.tab, self.user, self.assertEqual(tabs._wiki(self.tab, self.user,
self.course, self.active_page0)[0].is_active, self.course, self.active_page0)[0].is_active,
False) False)
@override_settings(WIKI_ENABLED=False) @override_settings(WIKI_ENABLED=False)
...@@ -129,14 +130,13 @@ class StaticTabTestCase(TestCase): ...@@ -129,14 +130,13 @@ class StaticTabTestCase(TestCase):
self.assertEqual(tabs._static_tab(self.tabby, self.user, self.assertEqual(tabs._static_tab(self.tabby, self.user,
self.course, self.active_page1)[0].link, self.course, self.active_page1)[0].link,
reverse('static_tab', args = [self.course.id, reverse('static_tab', args=[self.course.id,
self.tabby['url_slug']])) self.tabby['url_slug']]))
self.assertEqual(tabs._static_tab(self.tabby, self.user, self.assertEqual(tabs._static_tab(self.tabby, self.user,
self.course, self.active_page1)[0].is_active, self.course, self.active_page1)[0].is_active,
True) True)
self.assertEqual(tabs._static_tab(self.tabby, self.user, self.assertEqual(tabs._static_tab(self.tabby, self.user,
self.course, self.active_page0)[0].is_active, self.course, self.active_page0)[0].is_active,
False) False)
...@@ -183,7 +183,7 @@ class TextbooksTestCase(TestCase): ...@@ -183,7 +183,7 @@ class TextbooksTestCase(TestCase):
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page1)[1].name, self.course, self.active_page1)[1].name,
'Topology') 'Topology')
self.assertEqual(tabs._textbooks(self.tab, self.mockuser1, self.assertEqual(tabs._textbooks(self.tab, self.mockuser1,
self.course, self.active_page1)[1].link, self.course, self.active_page1)[1].link,
...@@ -206,6 +206,7 @@ class TextbooksTestCase(TestCase): ...@@ -206,6 +206,7 @@ class TextbooksTestCase(TestCase):
self.assertEqual(tabs._textbooks(self.tab, self.mockuser0, self.assertEqual(tabs._textbooks(self.tab, self.mockuser0,
self.course, self.active_pageX), []) self.course, self.active_pageX), [])
class KeyCheckerTestCase(TestCase): class KeyCheckerTestCase(TestCase):
def setUp(self): def setUp(self):
...@@ -223,39 +224,36 @@ class KeyCheckerTestCase(TestCase): ...@@ -223,39 +224,36 @@ class KeyCheckerTestCase(TestCase):
class NullValidatorTestCase(TestCase): class NullValidatorTestCase(TestCase):
def setUp(self): def setUp(self):
self.d = {}
def test_null_validator(self): self.dummy = {}
self.assertIsNone(tabs.null_validator(self.d)) def test_null_validator(self):
self.assertIsNone(tabs.null_validator(self.dummy))
class ValidateTabsTestCase(TestCase): class ValidateTabsTestCase(TestCase):
def setUp(self): def setUp(self):
self.courses = [MagicMock() for i in range(0,5)] self.courses = [MagicMock() for i in range(0, 5)]
self.courses[0].tabs = None self.courses[0].tabs = None
self.courses[1].tabs = [{'type':'courseware'}, {'type': 'fax'}] self.courses[1].tabs = [{'type': 'courseware'}, {'type': 'fax'}]
self.courses[2].tabs = [{'type':'shadow'}, {'type': 'course_info'}]
self.courses[3].tabs = [{'type':'courseware'},{'type':'course_info', 'name': 'alice'}, self.courses[2].tabs = [{'type': 'shadow'}, {'type': 'course_info'}]
{'type': 'wiki', 'name':'alice'}, {'type':'discussion', 'name': 'alice'},
{'type':'external_link', 'name': 'alice', 'link':'blink'},
{'type':'textbooks'}, {'type':'progress', 'name': 'alice'},
{'type':'static_tab', 'name':'alice', 'url_slug':'schlug'},
{'type': 'staff_grading'}]
self.courses[4].tabs = [{'type':'courseware'},{'type': 'course_info'}, {'type': 'flying'}] self.courses[3].tabs = [{'type': 'courseware'}, {'type': 'course_info', 'name': 'alice'},
{'type': 'wiki', 'name': 'alice'}, {'type': 'discussion', 'name': 'alice'},
{'type': 'external_link', 'name': 'alice', 'link': 'blink'},
{'type': 'textbooks'}, {'type': 'progress', 'name': 'alice'},
{'type': 'static_tab', 'name': 'alice', 'url_slug': 'schlug'},
{'type': 'staff_grading'}]
self.courses[4].tabs = [{'type': 'courseware'}, {'type': 'course_info'}, {'type': 'flying'}]
def test_validate_tabs(self): def test_validate_tabs(self):
self.assertIsNone(tabs.validate_tabs(self.courses[0])) self.assertIsNone(tabs.validate_tabs(self.courses[0]))
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[1]) self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[1])
self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2]) self.assertRaises(tabs.InvalidTabsException, tabs.validate_tabs, self.courses[2])
...@@ -268,15 +266,15 @@ class DiscussionLinkTestCase(ModuleStoreTestCase): ...@@ -268,15 +266,15 @@ class DiscussionLinkTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
self.tabs_with_discussion = [ self.tabs_with_discussion = [
{'type':'courseware'}, {'type': 'courseware'},
{'type':'course_info'}, {'type': 'course_info'},
{'type':'discussion'}, {'type': 'discussion'},
{'type':'textbooks'}, {'type': 'textbooks'},
] ]
self.tabs_without_discussion = [ self.tabs_without_discussion = [
{'type':'courseware'}, {'type': 'courseware'},
{'type':'course_info'}, {'type': 'course_info'},
{'type':'textbooks'}, {'type': 'textbooks'},
] ]
@staticmethod @staticmethod
......
...@@ -22,7 +22,7 @@ from django.conf import settings ...@@ -22,7 +22,7 @@ from django.conf import settings
from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import test_system from xmodule.tests import get_test_system
from xmodule.tests.test_logic import LogicTest from xmodule.tests.test_logic import LogicTest
...@@ -58,7 +58,7 @@ class VideoAlphaFactory(object): ...@@ -58,7 +58,7 @@ class VideoAlphaFactory(object):
descriptor = Mock(weight="1") descriptor = Mock(weight="1")
system = test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
VideoAlphaModule.location = location VideoAlphaModule.location = location
module = VideoAlphaModule(system, descriptor, model_data) module = VideoAlphaModule(system, descriptor, model_data)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment