Commit 3d7f8932 by Chris Dodge

Merge branch 'master' of github.com:MITx/mitx into feature/cdodge/auto-forum-provision

parents 02f97858 aca6cdba
...@@ -34,6 +34,7 @@ load-plugins= ...@@ -34,6 +34,7 @@ load-plugins=
# multiple time (only on the command line, not in the configuration file where # multiple time (only on the command line, not in the configuration file where
# it should appear only once). # it should appear only once).
disable= disable=
# C0301: Line too long
# W0141: Used builtin function 'map' # W0141: Used builtin function 'map'
# W0142: Used * or ** magic # W0142: Used * or ** magic
# R0201: Method could be a function # R0201: Method could be a function
...@@ -42,7 +43,7 @@ disable= ...@@ -42,7 +43,7 @@ disable=
# R0903: Too few public methods (1/2) # R0903: Too few public methods (1/2)
# R0904: Too many public methods # R0904: Too many public methods
# R0913: Too many arguments # R0913: Too many arguments
W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913 C0301,W0141,W0142,R0201,R0901,R0902,R0903,R0904,R0913
[REPORTS] [REPORTS]
......
...@@ -9,6 +9,9 @@ from xmodule.modulestore.django import _MODULESTORES, modulestore ...@@ -9,6 +9,9 @@ from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from auth.authz import get_user_by_email from auth.authz import get_user_by_email
from selenium.webdriver.common.keys import Keys
import time
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -140,3 +143,14 @@ def add_subsection(name='Subsection One'): ...@@ -140,3 +143,14 @@ def add_subsection(name='Subsection One'):
save_css = 'input.new-subsection-name-save' save_css = 'input.new-subsection-name-save'
world.css_fill(name_css, name) world.css_fill(name_css, name)
world.css_click(save_css) world.css_click(save_css)
def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(date_css, desired_date)
# hit TAB to get to the time field
e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal
from selenium.webdriver.common.keys import Keys
import time
############### ACTIONS #################### ############### ACTIONS ####################
...@@ -39,16 +37,8 @@ def i_click_the_edit_link_for_the_release_date(step): ...@@ -39,16 +37,8 @@ def i_click_the_edit_link_for_the_release_date(step):
@step('I save a new section release date$') @step('I save a new section release date$')
def i_save_a_new_section_release_date(step): def i_save_a_new_section_release_date(step):
date_css = 'input.start-date.date.hasDatepicker' set_date_and_time('input.start-date.date.hasDatepicker', '12/25/2013',
time_css = 'input.start-time.time.ui-timepicker-input' 'input.start-time.time.ui-timepicker-input', '12:00am')
world.css_fill(date_css, '12/25/2013')
# hit TAB to get to the time field
e = world.css_find(date_css).first
e._element.send_keys(Keys.TAB)
world.css_fill(time_css, '12:00am')
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
world.browser.click_link_by_text('Save') world.browser.click_link_by_text('Save')
......
...@@ -25,6 +25,13 @@ Feature: Create Subsection ...@@ -25,6 +25,13 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see it marked as Homework Then I see it marked as Homework
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I have set a release date and due date in different years
Then I see the correct dates
And I reload the page
Then I see the correct dates
@skip-phantom @skip-phantom
Scenario: Delete a subsection Scenario: Delete a subsection
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
...@@ -33,3 +40,5 @@ Feature: Create Subsection ...@@ -33,3 +40,5 @@ Feature: Create Subsection
When I press the "subsection" delete icon When I press the "subsection" delete icon
And I confirm the alert And I confirm the alert
Then the subsection does not exist Then the subsection does not exist
...@@ -16,6 +16,18 @@ def i_have_opened_a_new_course_section(step): ...@@ -16,6 +16,18 @@ def i_have_opened_a_new_course_section(step):
add_section() add_section()
@step('I have added a new subsection$')
def i_have_added_a_new_subsection(step):
add_subsection()
@step('I have opened a new subsection in Studio$')
def i_have_opened_a_new_subsection(step):
step.given('I have opened a new course section in Studio')
step.given('I have added a new subsection')
world.css_click('span.subsection-name-value')
@step('I click the New Subsection link') @step('I click the New Subsection link')
def i_click_the_new_subsection_link(step): def i_click_the_new_subsection_link(step):
world.css_click('a.new-subsection-item') world.css_click('a.new-subsection-item')
...@@ -43,9 +55,20 @@ def i_see_complete_subsection_name_with_quote_in_editor(step): ...@@ -43,9 +55,20 @@ def i_see_complete_subsection_name_with_quote_in_editor(step):
assert_equal(world.css_find(css).value, 'Subsection With "Quote"') assert_equal(world.css_find(css).value, 'Subsection With "Quote"')
@step('I have added a new subsection$') @step('I have set a release date and due date in different years$')
def i_have_added_a_new_subsection(step): def test_have_set_dates_in_different_years(step):
add_subsection() set_date_and_time('input#start_date', '12/25/2011', 'input#start_time', '3:00am')
world.css_click('.set-date')
# Use a year in the past so that current year will always be different.
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '4:00am')
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
assert_equal('3:00am', world.css_find('input#start_time').first.value)
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
assert_equal('4:00am', world.css_find('input#due_time').first.value)
@step('I mark it as Homework$') @step('I mark it as Homework$')
......
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import check_module_metadata_editability
from xmodule.course_module import CourseDescriptor
from request_cache.middleware import RequestCache
class Command(BaseCommand):
help = '''Enumerates through the course and find common errors'''
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("check_course requires one argument: <location>")
loc_str = args[0]
loc = CourseDescriptor.id_to_location(loc_str)
store = modulestore()
# setup a request cache so we don't throttle the DB with all the metadata inheritance requests
store.request_cache = RequestCache.get_request_cache()
course = store.get_item(loc, depth=3)
err_cnt = 0
def _xlint_metadata(module):
err_cnt = check_module_metadata_editability(module)
for child in module.get_children():
err_cnt = err_cnt + _xlint_metadata(child)
return err_cnt
err_cnt = err_cnt + _xlint_metadata(course)
# we've had a bug where the xml_attributes field can we rewritten as a string rather than a dict
def _check_xml_attributes_field(module):
err_cnt = 0
if hasattr(module, 'xml_attributes') and isinstance(module.xml_attributes, basestring):
print 'module = {0} has xml_attributes as a string. It should be a dict'.format(module.location.url())
err_cnt = err_cnt + 1
for child in module.get_children():
err_cnt = err_cnt + _check_xml_attributes_field(child)
return err_cnt
err_cnt = err_cnt + _check_xml_attributes_field(course)
# check for dangling discussion items, this can cause errors in the forums
def _get_discussion_items(module):
discussion_items = []
if module.location.category == 'discussion':
discussion_items = discussion_items + [module.location.url()]
for child in module.get_children():
discussion_items = discussion_items + _get_discussion_items(child)
return discussion_items
discussion_items = _get_discussion_items(course)
# now query all discussion items via get_items() and compare with the tree-traversal
queried_discussion_items = store.get_items(['i4x', course.location.org, course.location.course,
'discussion', None, None])
for item in queried_discussion_items:
if item.location.url() not in discussion_items:
print 'Found dangling discussion module = {0}'.format(item.location.url())
...@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore ...@@ -25,7 +25,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
...@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -85,6 +85,43 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_edit_unit_full(self): def test_edit_unit_full(self):
self.check_edit_unit('full') self.check_edit_unit('full')
def _get_draft_counts(self, item):
cnt = 1 if getattr(item, 'is_draft', False) else 0
for child in item.get_children():
cnt = cnt + self._get_draft_counts(child)
return cnt
def test_get_depth_with_drafts(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None)
# make sure no draft items have been returned
num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 0)
problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'problem', 'ps01-simple', None]))
# put into draft
modulestore('draft').clone_item(problem.location, problem.location)
# make sure we can query that item and verify that it is a draft
draft_problem = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'problem', 'ps01-simple', None]))
self.assertTrue(getattr(draft_problem,'is_draft', False))
#now requery with depth
course = modulestore('draft').get_item(Location(['i4x', 'edX', 'simple',
'course', '2012_Fall', None]), depth=None)
# make sure just one draft item have been returned
num_drafts = self._get_draft_counts(course)
self.assertEqual(num_drafts, 1)
def test_static_tab_reordering(self): def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
...@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -123,6 +160,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check that there's actually content in the 'question' field # check that there's actually content in the 'question' field
self.assertGreater(len(items[0].question),0) self.assertGreater(len(items[0].question),0)
def test_xlint_fails(self):
err_cnt = perform_xlint('common/test/data', ['full'])
self.assertGreater(err_cnt, 0)
def test_delete(self): def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full']) import_from_xml(modulestore(), 'common/test/data/', ['full'])
......
'''
Utilities for contentstore tests
'''
#pylint: disable=W0603
import json import json
import copy import copy
from uuid import uuid4 from uuid import uuid4
...@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase): ...@@ -17,36 +23,89 @@ class ModuleStoreTestCase(TestCase):
collection with templates before running the TestCase collection with templates before running the TestCase
and drops it they are finished. """ and drops it they are finished. """
def _pre_setup(self): @staticmethod
super(ModuleStoreTestCase, self)._pre_setup() def flush_mongo_except_templates():
'''
Delete everything in the module store except templates
'''
modulestore = xmodule.modulestore.django.modulestore()
# This query means: every item in the collection
# that is not a template
query = {"_id.course": {"$ne": "templates"}}
# Remove everything except templates
modulestore.collection.remove(query)
@staticmethod
def load_templates_if_necessary():
'''
Load templates into the modulestore only if they do not already exist.
We need the templates, because they are copied to create
XModules such as sections and problems
'''
modulestore = xmodule.modulestore.django.modulestore()
# Count the number of templates
query = {"_id.course": "templates"}
num_templates = modulestore.collection.find(query).count()
if num_templates < 1:
update_templates()
@classmethod
def setUpClass(cls):
'''
Flush the mongo store and set up templates
'''
# Use a uuid to differentiate # Use a uuid to differentiate
# the mongo collections on jenkins. # the mongo collections on jenkins.
self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE) cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
self.test_MODULESTORE = self.orig_MODULESTORE test_modulestore = cls.orig_modulestore
self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex test_modulestore['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex test_modulestore['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
settings.MODULESTORE = self.test_MODULESTORE
# Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
xmodule.modulestore.django._MODULESTORES = {} xmodule.modulestore.django._MODULESTORES = {}
update_templates()
settings.MODULESTORE = test_modulestore
TestCase.setUpClass()
@classmethod
def tearDownClass(cls):
'''
Revert to the old modulestore settings
'''
# Clean up by dropping the collection
modulestore = xmodule.modulestore.django.modulestore()
modulestore.collection.drop()
# Restore the original modulestore settings
settings.MODULESTORE = cls.orig_modulestore
def _pre_setup(self):
'''
Remove everything but the templates before each test
'''
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Check that we have templates loaded; if not, load them
ModuleStoreTestCase.load_templates_if_necessary()
# Call superclass implementation
super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self): def _post_teardown(self):
# Make sure you flush out the modulestore. '''
# Drop the collection at the end of the test, Flush everything we created except the templates
# otherwise there will be lingering collections leftover '''
# from executing the tests. # Flush anything that is not a template
xmodule.modulestore.django._MODULESTORES = {} ModuleStoreTestCase.flush_mongo_except_templates()
xmodule.modulestore.django.modulestore().collection.drop()
settings.MODULESTORE = self.orig_MODULESTORE
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown() super(ModuleStoreTestCase, self)._post_teardown()
......
import logging
from django.conf import settings from django.conf import settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
import copy
DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info']
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name" : "Open Ended Panel", "type" : "open_ended"}
def get_modulestore(location): def get_modulestore(location):
""" """
...@@ -137,7 +141,7 @@ def compute_unit_state(unit): ...@@ -137,7 +141,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS 'private' content is editabled and not visible in the LMS
""" """
if unit.cms.is_draft: if getattr(unit, 'is_draft', False):
try: try:
modulestore('direct').get_item(unit.location) modulestore('direct').get_item(unit.location)
return UnitState.draft return UnitState.draft
...@@ -147,10 +151,6 @@ def compute_unit_state(unit): ...@@ -147,10 +151,6 @@ def compute_unit_state(unit):
return UnitState.public return UnitState.public
def get_date_display(date):
return date.strftime("%d %B, %Y at %I:%M %p")
def update_item(location, value): def update_item(location, value):
""" """
If value is None, delete the db entry. Otherwise, update it using the correct modulestore. If value is None, delete the db entry. Otherwise, update it using the correct modulestore.
...@@ -191,3 +191,35 @@ class CoursePageNames: ...@@ -191,3 +191,35 @@ class CoursePageNames:
SettingsGrading = "settings_grading" SettingsGrading = "settings_grading"
CourseOutline = "course_index" CourseOutline = "course_index"
Checklists = "checklists" Checklists = "checklists"
def add_open_ended_panel_tab(course):
"""
Used to add the open ended panel tab to a course if it does not exist.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
#Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL not in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs.append(OPEN_ENDED_PANEL)
changed = True
return changed, course_tabs
def remove_open_ended_panel_tab(course):
"""
Used to remove the open ended panel tab from a course if it exists.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
#Copy course tabs
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct!=OPEN_ENDED_PANEL]
changed = True
return changed, course_tabs
...@@ -6,7 +6,6 @@ import sys ...@@ -6,7 +6,6 @@ import sys
import time import time
import tarfile import tarfile
import shutil import shutil
from datetime import datetime
from collections import defaultdict from collections import defaultdict
from uuid import uuid4 from uuid import uuid4
from path import path from path import path
...@@ -43,17 +42,19 @@ from xmodule.modulestore.mongo import MongoUsage ...@@ -43,17 +42,19 @@ from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule_modifiers import replace_static_urls, wrap_xmodule from xmodule_modifiers import replace_static_urls, wrap_xmodule
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError, ProcessingError
from functools import partial from functools import partial
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.util.date_utils import get_default_time_display
from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role from auth.authz import is_user_in_course_group_role, get_users_in_course_group_by_role
from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group from auth.authz import get_user_by_email, add_user_to_course_group, remove_user_from_course_group
from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups from auth.authz import INSTRUCTOR_ROLE_NAME, STAFF_ROLE_NAME, create_all_course_groups
from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \ from .utils import get_course_location_for_item, get_lms_link_for_item, compute_unit_state, \
get_date_display, UnitState, get_course_for_item, get_url_reverse UnitState, get_course_for_item, get_url_reverse, add_open_ended_panel_tab, \
remove_open_ended_panel_tab
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from contentstore.course_info_model import get_course_updates, \ from contentstore.course_info_model import get_course_updates, \
...@@ -74,7 +75,8 @@ log = logging.getLogger(__name__) ...@@ -74,7 +75,8 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
ADVANCED_COMPONENT_TYPES = ['annotatable', 'combinedopenended', 'peergrading'] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
ADVANCED_COMPONENT_TYPES = ['annotatable'] + OPEN_ENDED_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
...@@ -189,7 +191,7 @@ def course_index(request, org, course, name): ...@@ -189,7 +191,7 @@ def course_index(request, org, course, name):
'coursename': name 'coursename': name
}) })
course = modulestore().get_item(location) course = modulestore().get_item(location, depth=3)
sections = course.get_children() sections = course.get_children()
return render_to_response('overview.html', { return render_to_response('overview.html', {
...@@ -209,19 +211,14 @@ def course_index(request, org, course, name): ...@@ -209,19 +211,14 @@ def course_index(request, org, course, name):
@login_required @login_required
def edit_subsection(request, location): def edit_subsection(request, location):
# check that we have permissions to edit this item # check that we have permissions to edit this item
if not has_access(request.user, location): course = get_course_for_item(location)
if not has_access(request.user, course.location):
raise PermissionDenied() raise PermissionDenied()
item = modulestore().get_item(location) item = modulestore().get_item(location, depth=1)
# TODO: we need a smarter way to figure out what course an item is in lms_link = get_lms_link_for_item(location, course_id=course.location.course_id)
for course in modulestore().get_courses(): preview_link = get_lms_link_for_item(location, course_id=course.location.course_id, preview=True)
if (course.location.org == item.location.org and
course.location.course == item.location.course):
break
lms_link = get_lms_link_for_item(location)
preview_link = get_lms_link_for_item(location, preview=True)
# make sure that location references a 'sequential', otherwise return BadRequest # make sure that location references a 'sequential', otherwise return BadRequest
if item.location.category != 'sequential': if item.location.category != 'sequential':
...@@ -278,19 +275,13 @@ def edit_unit(request, location): ...@@ -278,19 +275,13 @@ def edit_unit(request, location):
id: A Location URL id: A Location URL
""" """
# check that we have permissions to edit this item course = get_course_for_item(location)
if not has_access(request.user, location): if not has_access(request.user, course.location):
raise PermissionDenied() raise PermissionDenied()
item = modulestore().get_item(location) item = modulestore().get_item(location, depth=1)
# TODO: we need a smarter way to figure out what course an item is in lms_link = get_lms_link_for_item(item.location, course_id=course.location.course_id)
for course in modulestore().get_courses():
if (course.location.org == item.location.org and
course.location.course == item.location.course):
break
lms_link = get_lms_link_for_item(item.location)
component_templates = defaultdict(list) component_templates = defaultdict(list)
...@@ -375,7 +366,7 @@ def edit_unit(request, location): ...@@ -375,7 +366,7 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link, 'published_preview_link': lms_link,
'subsection': containing_subsection, 'subsection': containing_subsection,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None, 'release_date': get_default_time_display(containing_subsection.lms.start) if containing_subsection.lms.start is not None else None,
'section': containing_section, 'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state, 'unit_state': unit_state,
...@@ -449,9 +440,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None): ...@@ -449,9 +440,16 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, request.POST) ajax_return = instance.handle_ajax(dispatch, request.POST)
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 raise Http404
except ProcessingError:
log.warning("Module raised an error while processing AJAX request",
exc_info=True)
return HttpResponseBadRequest()
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
...@@ -831,7 +829,7 @@ def upload_asset(request, org, course, coursename): ...@@ -831,7 +829,7 @@ def upload_asset(request, org, course, coursename):
readback = contentstore().find(content.location) readback = contentstore().find(content.location)
response_payload = {'displayname': content.name, response_payload = {'displayname': content.name,
'uploadDate': get_date_display(readback.last_modified_at), 'uploadDate': get_default_time_display(readback.last_modified_at.timetuple()),
'url': StaticContent.get_url_path_from_location(content.location), 'url': StaticContent.get_url_path_from_location(content.location),
'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None, 'thumb_url': StaticContent.get_url_path_from_location(thumbnail_location) if thumbnail_content is not None else None,
'msg': 'Upload completed' 'msg': 'Upload completed'
...@@ -1274,15 +1272,48 @@ def course_advanced_updates(request, org, course, name): ...@@ -1274,15 +1272,48 @@ def course_advanced_updates(request, org, course, name):
location = get_location_and_verify_access(request, org, course, name) location = get_location_and_verify_access(request, org, course, name)
real_method = get_request_method(request) real_method = get_request_method(request)
if real_method == 'GET': if real_method == 'GET':
return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json") return HttpResponse(json.dumps(CourseMetadata.fetch(location)), mimetype="application/json")
elif real_method == 'DELETE': elif real_method == 'DELETE':
return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))), mimetype="application/json") return HttpResponse(json.dumps(CourseMetadata.delete_key(location, json.loads(request.body))),
mimetype="application/json")
elif real_method == 'POST' or real_method == 'PUT': elif real_method == 'POST' or real_method == 'PUT':
# NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key # NOTE: request.POST is messed up because expect_json cloned_request.POST.copy() is creating a defective entry w/ the whole payload as the key
return HttpResponse(json.dumps(CourseMetadata.update_from_json(location, json.loads(request.body))), mimetype="application/json") request_body = json.loads(request.body)
#Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True
#Check to see if the user instantiated any advanced components. This is a hack to add the open ended panel tab
#to a course automatically if the user has indicated that they want to edit the combinedopenended or peergrading
#module, and to remove it if they have removed the open ended elements.
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
#Check to see if the user instantiated any open ended components
found_oe_type = False
#Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
#Add an open ended tab to the course if needed
changed, new_tabs = add_open_ended_panel_tab(course_module)
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed:
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
#Set this flag to avoid the open ended tab removal code below.
found_oe_type = True
break
#If we did not find an open ended module type in the advanced settings,
# we may need to remove the open ended tab from the course.
if not found_oe_type:
#Remove open ended tab to the course if needed
changed, new_tabs = remove_open_ended_panel_tab(course_module)
if changed:
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location, request_body, filter_tabs=filter_tabs))
return HttpResponse(response_json, mimetype="application/json")
@ensure_csrf_cookie @ensure_csrf_cookie
@login_required @login_required
...@@ -1403,7 +1434,7 @@ def asset_index(request, org, course, name): ...@@ -1403,7 +1434,7 @@ def asset_index(request, org, course, name):
id = asset['_id'] id = asset['_id']
display_info = {} display_info = {}
display_info['displayname'] = asset['displayname'] display_info['displayname'] = asset['displayname']
display_info['uploadDate'] = get_date_display(asset['uploadDate']) display_info['uploadDate'] = get_default_time_display(asset['uploadDate'].timetuple())
asset_location = StaticContent.compute_location(id['org'], id['course'], id['name']) asset_location = StaticContent.compute_location(id['org'], id['course'], id['name'])
display_info['url'] = StaticContent.get_url_path_from_location(asset_location) display_info['url'] = StaticContent.get_url_path_from_location(asset_location)
......
...@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor ...@@ -4,7 +4,7 @@ from xmodule.x_module import XModuleDescriptor
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xblock.core import Scope from xblock.core import Scope
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
import copy
class CourseMetadata(object): class CourseMetadata(object):
''' '''
...@@ -39,7 +39,7 @@ class CourseMetadata(object): ...@@ -39,7 +39,7 @@ class CourseMetadata(object):
return course return course
@classmethod @classmethod
def update_from_json(cls, course_location, jsondict): def update_from_json(cls, course_location, jsondict, filter_tabs=True):
""" """
Decode the json into CourseMetadata and save any changed attrs to the db. Decode the json into CourseMetadata and save any changed attrs to the db.
...@@ -48,10 +48,16 @@ class CourseMetadata(object): ...@@ -48,10 +48,16 @@ class CourseMetadata(object):
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
dirty = False dirty = False
#Copy the filtered list to avoid permanently changing the class attribute
filtered_list = copy.copy(cls.FILTERED_LIST)
#Don't filter on the tab attribute if filter_tabs is False
if not filter_tabs:
filtered_list.remove("tabs")
for k, v in jsondict.iteritems(): for k, v in jsondict.iteritems():
# should it be an error if one of the filtered list items is in the payload? # should it be an error if one of the filtered list items is in the payload?
if k in cls.FILTERED_LIST: if k in filtered_list:
continue continue
if hasattr(descriptor, k) and getattr(descriptor, k) != v: if hasattr(descriptor, k) and getattr(descriptor, k) != v:
......
...@@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') ...@@ -46,6 +46,9 @@ SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items(): for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value MITX_FEATURES[feature] = value
# load segment.io key, provide a dummy if it does not exist
SEGMENT_IO_KEY = ENV_TOKENS.get('SEGMENT_IO_KEY', '***REMOVED***')
LOGGING = get_logger_config(LOG_DIR, LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'], logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514), syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
......
...@@ -34,6 +34,9 @@ MITX_FEATURES = { ...@@ -34,6 +34,9 @@ MITX_FEATURES = {
'ENABLE_DISCUSSION_SERVICE': False, 'ENABLE_DISCUSSION_SERVICE': False,
'AUTH_USE_MIT_CERTIFICATES': False, 'AUTH_USE_MIT_CERTIFICATES': False,
'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests 'STUB_VIDEO_FOR_TESTING': False, # do not display video when running automated acceptance tests
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
......
...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0 ...@@ -112,6 +112,10 @@ CACHE_TIMEOUT = 0
# Dummy secret key for dev # Dummy secret key for dev
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
################################ PIPELINE #################################
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
################################ DEBUG TOOLBAR ################################# ################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo') INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware', MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
...@@ -142,4 +146,10 @@ DEBUG_TOOLBAR_CONFIG = { ...@@ -142,4 +146,10 @@ DEBUG_TOOLBAR_CONFIG = {
# To see stacktraces for MongoDB queries, set this to True. # To see stacktraces for MongoDB queries, set this to True.
# Stacktraces slow down page loads drastically (for pages with lots of queries). # Stacktraces slow down page loads drastically (for pages with lots of queries).
DEBUG_TOOLBAR_MONGO_STACKTRACES = False DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
# segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg'
...@@ -58,6 +58,10 @@ MODULESTORE = { ...@@ -58,6 +58,10 @@ MODULESTORE = {
'direct': { 'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': modulestore_options
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': modulestore_options
} }
} }
...@@ -114,3 +118,6 @@ PASSWORD_HASHERS = ( ...@@ -114,3 +118,6 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.SHA1PasswordHasher', 'django.contrib.auth.hashers.SHA1PasswordHasher',
'django.contrib.auth.hashers.MD5PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher',
) )
# dummy segment-io key
SEGMENT_IO_KEY = '***REMOVED***'
...@@ -15,7 +15,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -15,7 +15,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
$component_editor: => @$el.find('.component-editor') $component_editor: => @$el.find('.component-editor')
loadDisplay: -> loadDisplay: ->
XModule.loadModule(@$el.find('.xmodule_display')) XModule.loadModule(@$el.find('.xmodule_display'))
loadEdit: -> loadEdit: ->
if not @module if not @module
...@@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -55,6 +55,11 @@ class CMS.Views.ModuleEdit extends Backbone.View
clickSaveButton: (event) => clickSaveButton: (event) =>
event.preventDefault() event.preventDefault()
data = @module.save() data = @module.save()
analytics.track "Saved Module",
course: course_location_analytics
id: _this.model.id
data.metadata = _.extend(data.metadata || {}, @metadata()) data.metadata = _.extend(data.metadata || {}, @metadata())
@hideModal() @hideModal()
@model.save(data).done( => @model.save(data).done( =>
......
...@@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -28,6 +28,10 @@ class CMS.Views.TabsEdit extends Backbone.View
@$('.component').each((idx, element) => @$('.component').each((idx, element) =>
tabs.push($(element).data('id')) tabs.push($(element).data('id'))
) )
analytics.track "Reordered Static Pages",
course: course_location_analytics
$.ajax({ $.ajax({
type:'POST', type:'POST',
url: '/reorder_static_tabs', url: '/reorder_static_tabs',
...@@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View ...@@ -56,10 +60,18 @@ class CMS.Views.TabsEdit extends Backbone.View
'i4x://edx/templates/static_tab/Empty' 'i4x://edx/templates/static_tab/Empty'
) )
analytics.track "Added Static Page",
course: course_location_analytics
deleteTab: (event) => deleteTab: (event) =>
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.' if not confirm 'Are you sure you want to delete this component? This action cannot be undone.'
return return
$component = $(event.currentTarget).parents('.component') $component = $(event.currentTarget).parents('.component')
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
$.post('/delete_item', { $.post('/delete_item', {
id: $component.data('id') id: $component.data('id')
}, => }, =>
......
...@@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -35,6 +35,10 @@ class CMS.Views.UnitEdit extends Backbone.View
@$('.components').sortable( @$('.components').sortable(
handle: '.drag-handle' handle: '.drag-handle'
update: (event, ui) => update: (event, ui) =>
analytics.track "Reordered Components",
course: course_location_analytics
id: unit_location_analytics
payload = children : @components() payload = children : @components()
options = success : => @model.unset('children') options = success : => @model.unset('children')
@model.save(payload, options) @model.save(payload, options)
...@@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -89,6 +93,11 @@ class CMS.Views.UnitEdit extends Backbone.View
$(event.currentTarget).data('location') $(event.currentTarget).data('location')
) )
analytics.track "Added a Component",
course: course_location_analytics
unit_id: unit_location_analytics
type: $(event.currentTarget).data('location')
@closeNewComponent(event) @closeNewComponent(event)
components: => @$('.component').map((idx, el) -> $(el).data('id')).get() components: => @$('.component').map((idx, el) -> $(el).data('id')).get()
...@@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -111,6 +120,11 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/delete_item', { $.post('/delete_item', {
id: $component.data('id') id: $component.data('id')
}, => }, =>
analytics.track "Deleted a Component",
course: course_location_analytics
unit_id: unit_location_analytics
id: $component.data('id')
$component.remove() $component.remove()
# b/c we don't vigilantly keep children up to date # b/c we don't vigilantly keep children up to date
# get rid of it before it hurts someone # get rid of it before it hurts someone
...@@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -129,6 +143,10 @@ class CMS.Views.UnitEdit extends Backbone.View
id: @$el.data('id') id: @$el.data('id')
delete_children: true delete_children: true
}, => }, =>
analytics.track "Deleted Draft",
course: course_location_analytics
unit_id: unit_location_analytics
window.location.reload() window.location.reload()
) )
...@@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -138,6 +156,10 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/create_draft', { $.post('/create_draft', {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Created Draft",
course: course_location_analytics
unit_id: unit_location_analytics
@model.set('state', 'draft') @model.set('state', 'draft')
) )
...@@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View ...@@ -148,20 +170,31 @@ class CMS.Views.UnitEdit extends Backbone.View
$.post('/publish_draft', { $.post('/publish_draft', {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Published Draft",
course: course_location_analytics
unit_id: unit_location_analytics
@model.set('state', 'public') @model.set('state', 'public')
) )
setVisibility: (event) -> setVisibility: (event) ->
if @$('.visibility-select').val() == 'private' if @$('.visibility-select').val() == 'private'
target_url = '/unpublish_unit' target_url = '/unpublish_unit'
visibility = "private"
else else
target_url = '/publish_draft' target_url = '/publish_draft'
visibility = "public"
@wait(true) @wait(true)
$.post(target_url, { $.post(target_url, {
id: @$el.data('id') id: @$el.data('id')
}, => }, =>
analytics.track "Set Unit Visibility",
course: course_location_analytics
unit_id: unit_location_analytics
visibility: visibility
@model.set('state', @$('.visibility-select').val()) @model.set('state', @$('.visibility-select').val())
) )
...@@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View ...@@ -193,6 +226,11 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
@model.save(metadata: metadata) @model.save(metadata: metadata)
# Update name shown in the right-hand side location summary. # Update name shown in the right-hand side location summary.
$('.unit-location .editing .unit-name').html(metadata.display_name) $('.unit-location .editing .unit-name').html(metadata.display_name)
analytics.track "Edited Unit Name",
course: course_location_analytics
unit_id: unit_location_analytics
display_name: metadata.display_name
class CMS.Views.UnitEdit.LocationState extends Backbone.View class CMS.Views.UnitEdit.LocationState extends Backbone.View
initialize: => initialize: =>
......
...@@ -4,6 +4,9 @@ var $modalCover; ...@@ -4,6 +4,9 @@ var $modalCover;
var $newComponentItem; var $newComponentItem;
var $changedInput; var $changedInput;
var $spinner; var $spinner;
var $newComponentTypePicker;
var $newComponentTemplatePickers;
var $newComponentButton;
$(document).ready(function () { $(document).ready(function () {
$body = $('body'); $body = $('body');
...@@ -83,6 +86,8 @@ $(document).ready(function () { ...@@ -83,6 +86,8 @@ $(document).ready(function () {
// general link management - smooth scrolling page links // general link management - smooth scrolling page links
$('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink);
// tender feedback window scrolling
$('a.show-tender').bind('click', smoothScrollTop);
// toggling overview section details // toggling overview section details
$(function () { $(function () {
...@@ -160,6 +165,18 @@ function smoothScrollLink(e) { ...@@ -160,6 +165,18 @@ function smoothScrollLink(e) {
}); });
} }
function smoothScrollTop(e) {
(e).preventDefault();
$.smoothScroll({
offset: -200,
easing: 'swing',
speed: 1000,
scrollElement: null,
scrollTarget: $('#view-top')
});
}
function linkNewWindow(e) { function linkNewWindow(e) {
window.open($(e.target).attr('href')); window.open($(e.target).attr('href'));
e.preventDefault(); e.preventDefault();
...@@ -228,7 +245,7 @@ function syncReleaseDate(e) { ...@@ -228,7 +245,7 @@ function syncReleaseDate(e) {
$("#start_time").val(""); $("#start_time").val("");
} }
function getEdxTimeFromDateTimeVals(date_val, time_val, format) { function getEdxTimeFromDateTimeVals(date_val, time_val) {
var edxTimeStr = null; var edxTimeStr = null;
if (date_val != '') { if (date_val != '') {
...@@ -237,20 +254,17 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) { ...@@ -237,20 +254,17 @@ function getEdxTimeFromDateTimeVals(date_val, time_val, format) {
// Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing // Note, we are using date.js utility which has better parsing abilities than the built in JS date parsing
var date = Date.parse(date_val + " " + time_val); var date = Date.parse(date_val + " " + time_val);
if (format == null) edxTimeStr = date.toString('yyyy-MM-ddTHH:mm');
format = 'yyyy-MM-ddTHH:mm';
edxTimeStr = date.toString(format);
} }
return edxTimeStr; return edxTimeStr;
} }
function getEdxTimeFromDateTimeInputs(date_id, time_id, format) { function getEdxTimeFromDateTimeInputs(date_id, time_id) {
var input_date = $('#' + date_id).val(); var input_date = $('#' + date_id).val();
var input_time = $('#' + time_id).val(); var input_time = $('#' + time_id).val();
return getEdxTimeFromDateTimeVals(input_date, input_time, format); return getEdxTimeFromDateTimeVals(input_date, input_time);
} }
function autosaveInput(e) { function autosaveInput(e) {
...@@ -291,10 +305,8 @@ function saveSubsection() { ...@@ -291,10 +305,8 @@ function saveSubsection() {
} }
// Piece back together the date/time UI elements into one date/time string // Piece back together the date/time UI elements into one date/time string
// NOTE: our various "date/time" metadata elements don't always utilize the same formatting string
// so make sure we're passing back the correct format
metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time'); metadata['start'] = getEdxTimeFromDateTimeInputs('start_date', 'start_time');
metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time', 'MMMM dd HH:mm'); metadata['due'] = getEdxTimeFromDateTimeInputs('due_date', 'due_time');
$.ajax({ $.ajax({
url: "/save_item", url: "/save_item",
...@@ -316,8 +328,14 @@ function saveSubsection() { ...@@ -316,8 +328,14 @@ function saveSubsection() {
function createNewUnit(e) { function createNewUnit(e) {
e.preventDefault(); e.preventDefault();
parent = $(this).data('parent'); var parent = $(this).data('parent');
template = $(this).data('template'); var template = $(this).data('template');
analytics.track('Created a Unit', {
'course': course_location_analytics,
'parent_location': parent
});
$.post('/clone_item', $.post('/clone_item',
{'parent_location': parent, {'parent_location': parent,
...@@ -351,6 +369,12 @@ function _deleteItem($el) { ...@@ -351,6 +369,12 @@ function _deleteItem($el) {
var id = $el.data('id'); var id = $el.data('id');
analytics.track('Deleted an Item', {
'course': course_location_analytics,
'id': id
});
$.post('/delete_item', $.post('/delete_item',
{'id': id, 'delete_children': true, 'delete_all_versions': true}, {'id': id, 'delete_children': true, 'delete_all_versions': true},
function (data) { function (data) {
...@@ -414,6 +438,11 @@ function displayFinishedUpload(xhr) { ...@@ -414,6 +438,11 @@ function displayFinishedUpload(xhr) {
var html = Mustache.to_html(template, resp); var html = Mustache.to_html(template, resp);
$('table > tbody').prepend(html); $('table > tbody').prepend(html);
analytics.track('Uploaded a File', {
'course': course_location_analytics,
'asset_url': resp.url
});
} }
function markAsLoaded() { function markAsLoaded() {
...@@ -543,6 +572,11 @@ function saveNewSection(e) { ...@@ -543,6 +572,11 @@ function saveNewSection(e) {
var template = $saveButton.data('template'); var template = $saveButton.data('template');
var display_name = $(this).find('.new-section-name').val(); var display_name = $(this).find('.new-section-name').val();
analytics.track('Created a Section', {
'course': course_location_analytics,
'display_name': display_name
});
$.post('/clone_item', { $.post('/clone_item', {
'parent_location': parent, 'parent_location': parent,
'template': template, 'template': template,
...@@ -588,6 +622,12 @@ function saveNewCourse(e) { ...@@ -588,6 +622,12 @@ function saveNewCourse(e) {
return; return;
} }
analytics.track('Created a Course', {
'org': org,
'number': number,
'display_name': display_name
});
$.post('/create_new_course', { $.post('/create_new_course', {
'template': template, 'template': template,
'org': org, 'org': org,
...@@ -634,9 +674,14 @@ function saveNewSubsection(e) { ...@@ -634,9 +674,14 @@ function saveNewSubsection(e) {
var parent = $(this).find('.new-subsection-name-save').data('parent'); var parent = $(this).find('.new-subsection-name-save').data('parent');
var template = $(this).find('.new-subsection-name-save').data('template'); var template = $(this).find('.new-subsection-name-save').data('template');
var display_name = $(this).find('.new-subsection-name-input').val(); var display_name = $(this).find('.new-subsection-name-input').val();
analytics.track('Created a Subsection', {
'course': course_location_analytics,
'display_name': display_name
});
$.post('/clone_item', { $.post('/clone_item', {
'parent_location': parent, 'parent_location': parent,
'template': template, 'template': template,
...@@ -690,6 +735,13 @@ function saveEditSectionName(e) { ...@@ -690,6 +735,13 @@ function saveEditSectionName(e) {
return; return;
} }
analytics.track('Edited Section Name', {
'course': course_location_analytics,
'display_name': display_name,
'id': id
});
var $_this = $(this); var $_this = $(this);
// call into server to commit the new order // call into server to commit the new order
$.ajax({ $.ajax({
...@@ -729,6 +781,12 @@ function saveSetSectionScheduleDate(e) { ...@@ -729,6 +781,12 @@ function saveSetSectionScheduleDate(e) {
var id = $modal.attr('data-id'); var id = $modal.attr('data-id');
analytics.track('Edited Section Release Date', {
'course': course_location_analytics,
'id': id,
'start': start
});
// call into server to commit the new order // call into server to commit the new order
$.ajax({ $.ajax({
url: "/save_item", url: "/save_item",
......
...@@ -77,11 +77,18 @@ CMS.Views.Checklists = Backbone.View.extend({ ...@@ -77,11 +77,18 @@ CMS.Views.Checklists = Backbone.View.extend({
var task_index = $checkbox.data('task'); var task_index = $checkbox.data('task');
var model = this.collection.at(checklist_index); var model = this.collection.at(checklist_index);
model.attributes.items[task_index].is_checked = $task.hasClass(completed); model.attributes.items[task_index].is_checked = $task.hasClass(completed);
model.save({}, model.save({},
{ {
success : function() { success : function() {
var updatedTemplate = self.renderTemplate(model, checklist_index); var updatedTemplate = self.renderTemplate(model, checklist_index);
self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate); self.$el.find('#course-checklist'+checklist_index).first().replaceWith(updatedTemplate);
analytics.track('Toggled a Checklist Task', {
'course': course_location_analytics,
'task': model.attributes.items[task_index].short_description,
'state': model.attributes.items[task_index].is_checked
});
}, },
error : CMS.ServerError error : CMS.ServerError
}); });
......
...@@ -107,6 +107,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -107,6 +107,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// push change to display, hide the editor, submit the change // push change to display, hide the editor, submit the change
targetModel.save({}, {error : CMS.ServerError}); targetModel.save({}, {error : CMS.ServerError});
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Update', {
'course': course_location_analytics,
'date': this.dateEntry(event).val()
});
}, },
onCancel: function(event) { onCancel: function(event) {
...@@ -147,6 +152,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -147,6 +152,11 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
return; return;
} }
analytics.track('Deleted Course Update', {
'course': course_location_analytics,
'date': this.dateEntry(event).val()
});
var targetModel = this.eventModel(event); var targetModel = this.eventModel(event);
this.modelDom(event).remove(); this.modelDom(event).remove();
var cacheThis = this; var cacheThis = this;
...@@ -284,6 +294,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -284,6 +294,11 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
this.model.save({}, {error: CMS.ServerError}); this.model.save({}, {error: CMS.ServerError});
this.$form.hide(); this.$form.hide();
this.closeEditor(this); this.closeEditor(this);
analytics.track('Saved Course Handouts', {
'course': course_location_analytics
});
}, },
onCancel: function(event) { onCancel: function(event) {
......
...@@ -137,6 +137,10 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({ ...@@ -137,6 +137,10 @@ CMS.Views.Settings.Advanced = CMS.Views.ValidatingView.extend({
success : function() { success : function() {
self.render(); self.render();
self.showMessage(self.successful_changes); self.showMessage(self.successful_changes);
analytics.track('Saved Advanced Settings', {
'course': course_location_analytics
});
}, },
error : CMS.ServerError error : CMS.ServerError
}); });
......
...@@ -644,7 +644,7 @@ hr.divide { ...@@ -644,7 +644,7 @@ hr.divide {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 99999; z-index: 10000;
padding: 0 10px; padding: 0 10px;
border-radius: 3px; border-radius: 3px;
background: rgba(0, 0, 0, 0.85); background: rgba(0, 0, 0, 0.85);
......
...@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125); ...@@ -22,6 +22,7 @@ $black-t0: rgba(0,0,0,0.125);
$black-t1: rgba(0,0,0,0.25); $black-t1: rgba(0,0,0,0.25);
$black-t2: rgba(0,0,0,0.50); $black-t2: rgba(0,0,0,0.50);
$black-t3: rgba(0,0,0,0.75); $black-t3: rgba(0,0,0,0.75);
$white: rgb(255,255,255); $white: rgb(255,255,255);
$white-t0: rgba(255,255,255,0.125); $white-t0: rgba(255,255,255,0.125);
$white-t1: rgba(255,255,255,0.25); $white-t1: rgba(255,255,255,0.25);
......
...@@ -27,7 +27,8 @@ ...@@ -27,7 +27,8 @@
@import 'elements/forms'; @import 'elements/forms';
@import 'elements/modal'; @import 'elements/modal';
@import 'elements/alerts'; @import 'elements/alerts';
@import 'elements/jquery-ui-calendar'; @import 'elements/vendor';
@import 'elements/tender-widget';
// specific views // specific views
@import 'views/account'; @import 'views/account';
......
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
// specific elements - course nav // specific elements - course nav
.nav-course { .nav-course {
width: 335px; width: 285px;
margin-top: -($baseline/4); margin-top: -($baseline/4);
@include font-size(14); @include font-size(14);
......
// tender help/support widget
// ====================
#tender_frame, #tender_window {
background-image: none !important;
background: none;
}
#tender_window {
@include border-radius(3px);
@include box-shadow(0 2px 3px $shadow);
height: ($baseline*35) !important;
background: $white !important;
border: 1px solid $gray;
}
#tender_window {
padding: 0 !important;
}
#tender_frame {
background: $white;
}
#tender_closer {
color: $blue-l2 !important;
text-transform: uppercase;
&:hover {
color: $blue-l4 !important;
}
}
// ====================
// tender style overrides - not rendered through here, but an archive is needed
#tender_frame iframe html {
font-size: 62.5%;
}
.widget-layout {
font-family: 'Open Sans', sans-serif;
}
.widget-layout .search,
.widget-layout .tabs,
.widget-layout .footer,
.widget-layout .header h1 a {
display: none;
}
.widget-layout .header {
background: rgb(85, 151, 221);
padding: 10px 20px;
}
.widget-layout h1, .widget-layout h2, .widget-layout h3, .widget-layout h4, .widget-layout h5, .widget-layout h6, .widget-layout label {
font-weight: 600;
}
.widget-layout .header h1 {
font-size: 22px;
}
.widget-layout .content {
overflow: auto;
height: auto !important;
padding: 20px;
}
.widget-layout .flash {
margin: -10px 0 15px 0;
padding: 10px 20px !important;
background-image: none !important;
}
.widget-layout .flash-error {
background: rgb(178, 6, 16) !important;
color: rgb(255,255,255) !important;
}
.widget-layout label {
font-size: 14px;
margin-bottom: 5px;
color: #4c4c4c;
font-weight: 500;
}
.widget-layout input[type="text"], .widget-layout textarea {
padding: 10px;
font-size: 16px;
color: rgb(0,0,0) !important;
border: 1px solid #b0b6c2;
border-radius: 2px;
background-color: #edf1f5;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #edf1f5),color-stop(100%, #fdfdfe));
background-image: -webkit-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -moz-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -ms-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: -o-linear-gradient(top, #edf1f5,#fdfdfe);
background-image: linear-gradient(top, #edf1f5,#fdfdfe);
background-color: #edf1f5;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
box-shadow: 0 1px 2px rgba(0,0,0,0.1) inset;
}
.widget-layout input[type="text"]:focus, .widget-layout textarea:focus {
background-color: #fffcf1;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fffcf1),color-stop(100%, #fffefd));
background-image: -webkit-linear-gradient(top, #fffcf1,#fffefd);
background-image: -moz-linear-gradient(top, #fffcf1,#fffefd);
background-image: -ms-linear-gradient(top, #fffcf1,#fffefd);
background-image: -o-linear-gradient(top, #fffcf1,#fffefd);
background-image: linear-gradient(top, #fffcf1,#fffefd);
outline: 0;
}
.widget-layout textarea {
width: 97%;
}
.widget-layout p.note {
text-align: right !important;
display: inline-block !important;
position: absolute !important;
right: -130px !important;
top: -5px !important;
font-size: 13px !important;
opacity: 0.80;
}
.widget-layout .form-actions {
margin: 15px 0;
border: none;
padding: 0;
}
.widget-layout dl.form {
float: none;
width: 100%;
border-bottom: 1px solid #f2f2f2;
margin-bottom: 10px;
padding-bottom: 10px;
}
.widget-layout dl.form:last-child {
border: none;
padding-bottom: 0;
margin-bottom: 20px;
}
.widget-layout dl.form dt, .widget-layout dl.form dd {
display: inline-block;
vertical-align: middle;
}
.widget-layout dl.form dt {
margin-right: 15px;
width: 70px;
}
.widget-layout dl.form dd {
width: 65%;
position: relative;
}
// specific elements
.widget-layout #discussion_body {
}
.widget-layout #discussion_body:before {
content: "What Question or Feedback Would You Like to Share?";
display: block;
font-size: 14px;
margin-bottom: 5px;
color: #4c4c4c;
font-weight: 500;
}
.widget-layout dl#brain_buster_captcha {
float: none;
width: 100%;
border-top: 1px solid #f2f2f2;
margin-top: 10px;
padding-top: 10px;
}
.widget-layout dl#brain_buster_captcha dd {
display: block !important;
}
.widget-layout dl#brain_buster_captcha #captcha_answer {
border-color: #333;
}
.widget-layout dl#brain_buster_captcha dd label {
display: block;
font-weight: 700;
margin: 0 15px 5px 0 !important;
}
.widget-layout dl#brain_buster_captcha dd #captcha_answer {
display: block;
width: 97%%;
}
.widget-layout .form-actions .btn-post_topic {
display: block;
width: 100%;
height: auto !important;
font-size: 16px;
font-weight: 700;
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
-moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset,0 0 0 rgba(0,0,0,0);
-webkit-transition-property: background-color,0.15s;
-moz-transition-property: background-color,0.15s;
-ms-transition-property: background-color,0.15s;
-o-transition-property: background-color,0.15s;
transition-property: background-color,0.15s;
-webkit-transition-duration: box-shadow,0.15s;
-moz-transition-duration: box-shadow,0.15s;
-ms-transition-duration: box-shadow,0.15s;
-o-transition-duration: box-shadow,0.15s;
transition-duration: box-shadow,0.15s;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
-ms-transition-timing-function: ease-out;
-o-transition-timing-function: ease-out;
transition-timing-function: ease-out;
-webkit-transition-delay: 0;
-moz-transition-delay: 0;
-ms-transition-delay: 0;
-o-transition-delay: 0;
transition-delay: 0;
border: 1px solid #34854c;
border-radius: 3px;
background-color: rgba(255,255,255,0.3);
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255,255,255,0.3)),color-stop(100%, rgba(255,255,255,0)));
background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -moz-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -ms-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: -o-linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-image: linear-gradient(top, rgba(255,255,255,0.3),rgba(255,255,255,0));
background-color: #25b85a;
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
-moz-box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset;
color: #fff;
text-align: center;
margin-top: 20px;
padding: 10px 20px;
}
.widget-layout .form-actions #private-discussion-opt {
float: none;
text-align: left;
margin: 0 0 15px 0;
}
.widget-layout .form-actions .btn-post_topic:hover, .widget-layout .form-actions .btn-post_topic:active {
background-color: #16ca57;
color: #fff;
}
\ No newline at end of file
// studio - elements - JQUI calendar // studio - elements - vendor overrides
// ==================== // ====================
// JQUI calendar
.ui-datepicker { .ui-datepicker {
border-color: $darkGrey; border-color: $darkGrey;
border-radius: 2px; border-radius: 2px;
...@@ -8,6 +9,7 @@ ...@@ -8,6 +9,7 @@
font-family: $sans-serif; font-family: $sans-serif;
font-size: 12px; font-size: 12px;
@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1)); @include box-shadow(0 5px 10px rgba(0, 0, 0, 0.1));
z-index: 100000 !important;
.ui-widget-header { .ui-widget-header {
background: $darkGrey; background: $darkGrey;
...@@ -53,4 +55,11 @@ ...@@ -53,4 +55,11 @@
border-color: $orange; border-color: $orange;
color: #fff; color: #fff;
} }
}
// ====================
// JQUI timepicker
.ui-timepicker-list {
z-index: 100000 !important;
} }
\ No newline at end of file
...@@ -26,7 +26,7 @@ body.course.outline { ...@@ -26,7 +26,7 @@ body.course.outline {
position: relative; position: relative;
top: -4px; top: -4px;
right: 50px; right: 50px;
width: 145px; width: 100px;
.status-label { .status-label {
position: absolute; position: absolute;
...@@ -62,7 +62,7 @@ body.course.outline { ...@@ -62,7 +62,7 @@ body.course.outline {
opacity: 0.0; opacity: 0.0;
position: absolute; position: absolute;
top: -1px; top: -1px;
left: 5px; right: 0;
margin: 0; margin: 0;
padding: 8px 12px; padding: 8px 12px;
background: $white; background: $white;
...@@ -160,7 +160,7 @@ body.course.outline { ...@@ -160,7 +160,7 @@ body.course.outline {
.section-published-date { .section-published-date {
position: absolute; position: absolute;
top: 19px; top: 19px;
right: 90px; right: 80px;
padding: 4px 10px; padding: 4px 10px;
border-radius: 3px; border-radius: 3px;
background: $lightGrey; background: $lightGrey;
...@@ -271,8 +271,6 @@ body.course.outline { ...@@ -271,8 +271,6 @@ body.course.outline {
.section-published-date { .section-published-date {
float: right; float: right;
width: 265px;
margin-right: 220px;
@include border-radius(3px); @include border-radius(3px);
background: $lightGrey; background: $lightGrey;
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-symbolicons-block.css')}" />
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('css/vendor/symbolset.ss-standard.css')}" />
<%include file="widgets/segment-io.html" />
<%block name="header_extras"></%block> <%block name="header_extras"></%block>
</head> </head>
...@@ -55,9 +57,11 @@ ...@@ -55,9 +57,11 @@
<%block name="content"></%block> <%block name="content"></%block>
<%include file="widgets/footer.html" /> <%include file="widgets/footer.html" />
<%include file="widgets/tender.html" />
<%block name="jsextra"></%block> <%block name="jsextra"></%block>
</body> </body>
<%include file="widgets/qualaroo.html" />
</html> </html>
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
from time import mktime
import dateutil.parser
import logging import logging
from datetime import datetime from xmodule.util.date_utils import get_time_struct_display
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
...@@ -13,7 +11,6 @@ ...@@ -13,7 +11,6 @@
<%namespace name="units" file="widgets/units.html" /> <%namespace name="units" file="widgets/units.html" />
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%namespace name='datetime' module='datetime'/>
<%block name="content"> <%block name="content">
<div class="main-wrapper"> <div class="main-wrapper">
...@@ -38,18 +35,15 @@ ...@@ -38,18 +35,15 @@
<div class="scheduled-date-input row"> <div class="scheduled-date-input row">
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label> <label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript"> <div class="datepair" data-language="javascript">
<% <input type="text" id="start_date" name="start_date" value="${get_time_struct_display(subsection.lms.start, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None <input type="text" id="start_time" name="start_time" value="${get_time_struct_display(subsection.lms.start, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
%>
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div> </div>
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start: % if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_start_date is None: % if parent_item.lms.start is None:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset. <p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default}, which is unset.
% else: % else:
<p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}. <p class="notice">The date above differs from the release date of ${parent_item.display_name_with_default} –
${get_time_struct_display(parent_item.lms.start, '%m/%d/%Y at %I:%M %p')}.
% endif % endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p> <a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name_with_default}.</a></p>
% endif % endif
...@@ -66,12 +60,8 @@ ...@@ -66,12 +60,8 @@
<a href="#" class="set-date">Set a due date</a> <a href="#" class="set-date">Set a due date</a>
<div class="datepair date-setter"> <div class="datepair date-setter">
<p class="date-description"> <p class="date-description">
<% <input type="text" id="due_date" name="due_date" value="${get_time_struct_display(subsection.lms.due, '%m/%d/%Y')}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use <input type="text" id="due_time" name="due_time" value="${get_time_struct_display(subsection.lms.due, '%H:%M')}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due else None
%>
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
<a href="#" class="remove-date">Remove due date</a> <a href="#" class="remove-date">Remove due date</a>
</p> </p>
</div> </div>
......
...@@ -46,6 +46,8 @@ ...@@ -46,6 +46,8 @@
<li class="nav-item"> <li class="nav-item">
% if not disable_course_creation: % if not disable_course_creation:
<a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Course</a> <a href="#" class="button new-button new-course-button"><i class="ss-icon ss-symbolicons-standard icon icon-create">&#x002B;</i> New Course</a>
% elif settings.MITX_FEATURES.get('STAFF_EMAIL',''):
<a href="mailto:${settings.MITX_FEATURES.get('STAFF_EMAIL','')}">Email staff to create course</a>
% endif % endif
</li> </li>
</ul> </ul>
...@@ -67,7 +69,7 @@ ...@@ -67,7 +69,7 @@
<article class="my-classes"> <article class="my-classes">
% if user.is_active: % if user.is_active:
<ul class="class-list"> <ul class="class-list">
%for course, url, lms_link in courses: %for course, url, lms_link in sorted(courses, key=lambda s: s[0].lower()):
<li> <li>
<a class="class-link" href="${url}" class="class-name"> <a class="class-link" href="${url}" class="class-name">
<span class="class-name">${course}</span> <span class="class-name">${course}</span>
......
<%inherit file="base.html" /> <%inherit file="base.html" />
<%! <%!
from time import mktime
import dateutil.parser
import logging import logging
from datetime import datetime from xmodule.util.date_utils import get_time_struct_display
%> %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%block name="title">Course Outline</%block> <%block name="title">Course Outline</%block>
...@@ -163,11 +161,10 @@ ...@@ -163,11 +161,10 @@
</h3> </h3>
<div class="section-published-date"> <div class="section-published-date">
<% <%
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None start_date_str = get_time_struct_display(section.lms.start, '%m/%d/%Y')
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else '' start_time_str = get_time_struct_display(section.lms.start, '%I:%M %p')
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%> %>
%if start_date is None: %if section.lms.start is None:
<span class="published-status">This section has not been released.</span> <span class="published-status">This section has not been released.</span>
<a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a> <a href="#" class="schedule-button" data-date="" data-time="" data-id="${section.location}">Schedule</a>
%else: %else:
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
}); });
}); });
var unit_location_analytics = '${unit_location}';
</script> </script>
</%block> </%block>
......
...@@ -14,15 +14,14 @@ ...@@ -14,15 +14,14 @@
<li class="nav-item nav-peripheral-pp"> <li class="nav-item nav-peripheral-pp">
<a href="#">Privacy Policy</a> <a href="#">Privacy Policy</a>
</li> --> </li> -->
<li class="nav-item nav-peripheral-help"> <li class="nav-item nav-peripheral-help">
<a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a> <a href="http://help.edge.edx.org/" rel="external">edX Studio Help</a>
</li> </li>
<li class="nav-item nav-peripheral-contact">
<a href="https://www.edx.org/contact" rel="external">Contact edX</a>
</li>
% if user.is_authenticated(): % if user.is_authenticated():
<!-- add in zendesk/tender feedback form UI --> <li class="nav-item nav-peripheral-feedback">
<a class="show-tender" href="http://help.edge.edx.org/discussion/new" title="Use our feedback tool, Tender, to share your feedback">Contact Us</a>
</li>
% endif % endif
</ol> </ol>
</nav> </nav>
......
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<div class="wrapper-header wrapper"> <div class="wrapper-header wrapper" id="view-top">
<header class="primary" role="banner"> <header class="primary" role="banner">
<div class="wrapper wrapper-left "> <div class="wrapper wrapper-left ">
......
% if settings.MITX_FEATURES.get('STUDIO_NPS_SURVEY'):
<!-- Qualaroo is used for net promoter score surveys -->
<script type="text/javascript">
% if user.is_authenticated():
var _kiq = _kiq || [];
_kiq.push(['identify', "${ user.email }" ]);
% endif
</script>
<!-- Qualaroo for edx.org -->
<script type="text/javascript" src="//s3.amazonaws.com/ki.js/48221/9SN.js" async="true"></script>
<!-- end Qualaroo -->
% endif
% if settings.MITX_FEATURES.get('SEGMENT_IO'):
<!-- begin Segment.io -->
<script type="text/javascript">
// if inside course, inject the course location into the JS namespace
%if context_course:
var course_location_analytics = "${context_course.location}";
%endif
var analytics=analytics||[];analytics.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=("https:"===document.location.protocol?"https://":"http://")+"d2dq2ahtl5zl1z.cloudfront.net/analytics.js/v1/"+e+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n);var r=function(e){return function(){analytics.push([e].concat(Array.prototype.slice.call(arguments,0)))}},i=["identify","track","trackLink","trackForm","trackClick","trackSubmit","pageview","ab","alias","ready"];for(var s=0;s<i.length;s++)analytics[i[s]]=r(i[s])};
analytics.load("${ settings.SEGMENT_IO_KEY }");
% if user.is_authenticated():
analytics.identify("${ user.id }", {
email : "${ user.email }",
username : "${ user.username }"
});
% endif
</script>
<!-- end Segment.io -->
% else:
<!-- dummy segment.io -->
<script type="text/javascript">
%if context_course:
var course_location_analytics = "${context_course.location}";
%endif
var analytics = {
track: function() { return; }
};
</script>
<!-- end dummy segment.io -->
% endif
% if user.is_authenticated():
<script type="text/javascript">
Tender = {
hideToggle: true,
title: '',
body: '',
hide_kb: 'true',
widgetToggles: $('.show-tender')
}
</script>
<script src="https://edxedge.tenderapp.com/tender_widget.js" type="text/javascript"></script>
% endif
\ No newline at end of file
...@@ -40,7 +40,6 @@ class CmsNamespace(Namespace): ...@@ -40,7 +40,6 @@ class CmsNamespace(Namespace):
""" """
Namespace with fields common to all blocks in Studio Namespace with fields common to all blocks in Studio
""" """
is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings) published_by = String(help="Id of the user who published this module", scope=Scope.settings)
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False) empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
#pylint: disable=C0111 #pylint: disable=C0111
#pylint: disable=W0621 #pylint: disable=W0621
# Disable the "wildcard import" warning so we can bring in all methods from
# course helpers and ui helpers
#pylint: disable=W0401
# Disable the "Unused import %s from wildcard import" warning
#pylint: disable=W0614
# Disable the "unused argument" warning because lettuce uses "step"
#pylint: disable=W0613
from lettuce import world, step from lettuce import world, step
from .course_helpers import * from .course_helpers import *
from .ui_helpers import * from .ui_helpers import *
from lettuce.django import django_url from lettuce.django import django_url
from nose.tools import assert_equals, assert_in from nose.tools import assert_equals, assert_in
import time
from logging import getLogger from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -125,11 +134,6 @@ def i_am_logged_in(step): ...@@ -125,11 +134,6 @@ def i_am_logged_in(step):
world.browser.visit(django_url('/')) world.browser.visit(django_url('/'))
@step('I am not logged in$')
def i_am_not_logged_in(step):
world.browser.cookies.delete()
@step(u'I am an edX user$') @step(u'I am an edX user$')
def i_am_an_edx_user(step): def i_am_an_edx_user(step):
world.create_user('robot') world.create_user('robot')
......
...@@ -32,6 +32,8 @@ from copy import deepcopy ...@@ -32,6 +32,8 @@ from copy import deepcopy
import chem import chem
import chem.miller import chem.miller
import chem.chemcalc
import chem.chemtools
import verifiers import verifiers
import verifiers.draganddrop import verifiers.draganddrop
...@@ -67,6 +69,9 @@ global_context = {'random': random, ...@@ -67,6 +69,9 @@ global_context = {'random': random,
'scipy': scipy, 'scipy': scipy,
'calc': calc, 'calc': calc,
'eia': eia, 'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop} 'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
...@@ -118,7 +123,7 @@ class LoncapaProblem(object): ...@@ -118,7 +123,7 @@ class LoncapaProblem(object):
# 3. Assign from the OS's random number generator # 3. Assign from the OS's random number generator
self.seed = state.get('seed', seed) self.seed = state.get('seed', seed)
if self.seed is None: if self.seed is None:
self.seed = struct.unpack('i', os.urandom(4)) self.seed = struct.unpack('i', os.urandom(4))[0]
self.student_answers = state.get('student_answers', {}) self.student_answers = state.get('student_answers', {})
if 'correct_map' in state: if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map']) self.correct_map.set_dict(state['correct_map'])
......
...@@ -80,16 +80,17 @@ class CorrectMap(object): ...@@ -80,16 +80,17 @@ class CorrectMap(object):
Special migration case: Special migration case:
If correct_map is a one-level dict, then convert it to the new dict of dicts format. If correct_map is a one-level dict, then convert it to the new dict of dicts format.
''' '''
if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict): # empty current dict
# empty current dict self.__init__()
self.__init__()
# create new dict entries # create new dict entries
if correct_map and not isinstance(correct_map.values()[0], dict):
# special migration
for k in correct_map: for k in correct_map:
self.set(k, correct_map[k]) self.set(k, correctness=correct_map[k])
else: else:
self.__init__()
for k in correct_map: for k in correct_map:
self.set(k, **correct_map[k]) self.set(k, **correct_map[k])
......
...@@ -655,9 +655,9 @@ class MatlabInput(CodeInput): ...@@ -655,9 +655,9 @@ class MatlabInput(CodeInput):
# Check if problem has been queued # Check if problem has been queued
self.queuename = 'matlab' self.queuename = 'matlab'
self.queue_msg = '' self.queue_msg = ''
if 'queue_msg' in self.input_state and self.status in ['queued','incomplete', 'unsubmitted']: if 'queue_msg' in self.input_state and self.status in ['queued', 'incomplete', 'unsubmitted']:
self.queue_msg = self.input_state['queue_msg'] self.queue_msg = self.input_state['queue_msg']
if 'queued' in self.input_state and self.input_state['queuestate'] is not None: if 'queuestate' in self.input_state and self.input_state['queuestate'] == 'queued':
self.status = 'queued' self.status = 'queued'
self.queue_len = 1 self.queue_len = 1
self.msg = self.plot_submitted_msg self.msg = self.plot_submitted_msg
...@@ -702,7 +702,7 @@ class MatlabInput(CodeInput): ...@@ -702,7 +702,7 @@ class MatlabInput(CodeInput):
def _extra_context(self): def _extra_context(self):
''' Set up additional context variables''' ''' Set up additional context variables'''
extra_context = { extra_context = {
'queue_len': self.queue_len, 'queue_len': str(self.queue_len),
'queue_msg': self.queue_msg 'queue_msg': self.queue_msg
} }
return extra_context return extra_context
...@@ -1140,12 +1140,13 @@ registry.register(DesignProtein2dInput) ...@@ -1140,12 +1140,13 @@ registry.register(DesignProtein2dInput)
class EditAGeneInput(InputTypeBase): class EditAGeneInput(InputTypeBase):
""" """
An input type for editing a gene. Integrates with the genex java applet. An input type for editing a gene.
Integrates with the genex GWT application.
Example: Example:
<editagene width="800" hight="500" dna_sequence="ETAAGGCTATAACCGA" /> <editagene genex_dna_sequence="CGAT" genex_problem_number="1"/>
""" """
template = "editageneinput.html" template = "editageneinput.html"
tags = ['editageneinput'] tags = ['editageneinput']
...@@ -1155,9 +1156,7 @@ class EditAGeneInput(InputTypeBase): ...@@ -1155,9 +1156,7 @@ class EditAGeneInput(InputTypeBase):
""" """
Note: width, height, and dna_sequencee are required. Note: width, height, and dna_sequencee are required.
""" """
return [Attribute('width'), return [Attribute('genex_dna_sequence'),
Attribute('height'),
Attribute('dna_sequence'),
Attribute('genex_problem_number') Attribute('genex_problem_number')
] ]
......
...@@ -17,6 +17,7 @@ import logging ...@@ -17,6 +17,7 @@ import logging
import numbers import numbers
import numpy import numpy
import os import os
import sys
import random import random
import re import re
import requests import requests
...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception): ...@@ -52,12 +53,17 @@ class LoncapaProblemError(Exception):
class ResponseError(Exception): class ResponseError(Exception):
''' '''
Error for failure in processing a response Error for failure in processing a response, including
exceptions that occur when executing a custom script.
''' '''
pass pass
class StudentInputError(Exception): class StudentInputError(Exception):
'''
Error for an invalid student input.
For example, submitting a string when the problem expects a number
'''
pass pass
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse): ...@@ -833,7 +839,7 @@ class NumericalResponse(LoncapaResponse):
import sys import sys
type, value, traceback = sys.exc_info() type, value, traceback = sys.exc_info()
raise StudentInputError, ("Invalid input: could not interpret '%s' as a number" % raise StudentInputError, ("Could not interpret '%s' as a number" %
cgi.escape(student_answer)), traceback cgi.escape(student_answer)), traceback
if correct: if correct:
...@@ -1072,13 +1078,10 @@ def sympy_check2(): ...@@ -1072,13 +1078,10 @@ def sympy_check2():
correct = self.context['correct'] correct = self.context['correct']
messages = self.context['messages'] messages = self.context['messages']
overall_message = self.context['overall_message'] overall_message = self.context['overall_message']
except Exception as err: except Exception as err:
print "oops in customresponse (code) error %s" % err self._handle_exec_exception(err)
print "context = ", self.context
print traceback.format_exc()
# Notify student
raise StudentInputError(
"Error: Problem could not be evaluated with your input")
else: else:
# self.code is not a string; assume its a function # self.code is not a string; assume its a function
...@@ -1105,13 +1108,9 @@ def sympy_check2(): ...@@ -1105,13 +1108,9 @@ def sympy_check2():
nargs, args, kwargs)) nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs) ret = fn(*args[:nargs], **kwargs)
except Exception as err: except Exception as err:
log.error("oops in customresponse (cfn) error %s" % err) self._handle_exec_exception(err)
# print "context = ",self.context
log.error(traceback.format_exc())
raise Exception("oops in customresponse (cfn) error %s" % err)
log.debug(
"[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
if type(ret) == dict: if type(ret) == dict:
...@@ -1147,9 +1146,9 @@ def sympy_check2(): ...@@ -1147,9 +1146,9 @@ def sympy_check2():
correct = [] correct = []
messages = [] messages = []
for input_dict in input_list: for input_dict in input_list:
correct.append('correct' correct.append('correct'
if input_dict['ok'] else 'incorrect') if input_dict['ok'] else 'incorrect')
msg = (self.clean_message_html(input_dict['msg']) msg = (self.clean_message_html(input_dict['msg'])
if 'msg' in input_dict else None) if 'msg' in input_dict else None)
messages.append(msg) messages.append(msg)
...@@ -1157,7 +1156,7 @@ def sympy_check2(): ...@@ -1157,7 +1156,7 @@ def sympy_check2():
# Raise an exception # Raise an exception
else: else:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise Exception( raise ResponseError(
"CustomResponse: check function returned an invalid dict") "CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value, # The check function can return a boolean value,
...@@ -1174,7 +1173,7 @@ def sympy_check2(): ...@@ -1174,7 +1173,7 @@ def sympy_check2():
correct_map.set_overall_message(overall_message) correct_map.set_overall_message(overall_message)
for k in range(len(idset)): for k in range(len(idset)):
npoints = (self.maxpoints[idset[k]] npoints = (self.maxpoints[idset[k]]
if correct[k] == 'correct' else 0) if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k], correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints) npoints=npoints)
...@@ -1227,6 +1226,22 @@ def sympy_check2(): ...@@ -1227,6 +1226,22 @@ def sympy_check2():
return {self.answer_ids[0]: self.expect} return {self.answer_ids[0]: self.expect}
return self.default_answer_map return self.default_answer_map
def _handle_exec_exception(self, err):
'''
Handle an exception raised during the execution of
custom Python code.
Raises a ResponseError
'''
# Log the error if we are debugging
msg = 'Error occurred while evaluating CustomResponse'
log.warning(msg, exc_info=True)
# Notify student with a student input error
_, _, traceback_obj = sys.exc_info()
raise ResponseError, err.message, traceback_obj
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse): ...@@ -1901,7 +1916,14 @@ class SchematicResponse(LoncapaResponse):
submission = [json.loads(student_answers[ submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)] k]) for k in sorted(self.answer_ids)]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
exec self.code in global_context, self.context
try:
exec self.code in global_context, self.context
except Exception as err:
_, _, traceback_obj = sys.exc_info()
raise ResponseError, ResponseError(err.message), traceback_obj
cmap = CorrectMap() cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted( cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct']))) self.answer_ids), self.context['correct'])))
...@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse): ...@@ -2106,7 +2128,7 @@ class AnnotationResponse(LoncapaResponse):
option_scoring = dict([(option['id'], { option_scoring = dict([(option['id'], {
'correctness': choices.get(option['choice']), 'correctness': choices.get(option['choice']),
'points': scoring.get(option['choice']) 'points': scoring.get(option['choice'])
}) for option in self._find_options(inputfield) ]) }) for option in self._find_options(inputfield)])
scoring_map[inputfield.get('id')] = option_scoring scoring_map[inputfield.get('id')] = option_scoring
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <label for="input_${id}_${choice_id}"
% if input_type == 'radio' and choice_id == value: % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
<% <%
if status == 'correct': if status == 'correct':
correctness = 'correct' correctness = 'correct'
...@@ -30,9 +30,9 @@ ...@@ -30,9 +30,9 @@
class="choicegroup_${correctness}" class="choicegroup_${correctness}"
% endif % endif
% endif % endif
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if input_type == 'radio' and choice_id == value: % if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
checked="true" checked="true"
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
% endif % endif
<div id="genex_container"></div> <div id="genex_container"></div>
<input type="hidden" name="dna_sequence" id="dna_sequence" value ="${dna_sequence}"></input> <input type="hidden" name="genex_dna_sequence" id="genex_dna_sequence" value ="${genex_dna_sequence}"></input>
<input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input> <input type="hidden" name="genex_problem_number" id="genex_problem_number" value ="${genex_problem_number}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" id="input_${id}" value="${value|h}"/>
......
...@@ -361,7 +361,6 @@ class MatlabTest(unittest.TestCase): ...@@ -361,7 +361,6 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, } 'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml) elt = etree.fromstring(self.xml)
input_class = lookup_tag('matlabinput')
the_input = self.input_class(test_system, elt, state) the_input = self.input_class(test_system, elt, state)
context = the_input._get_render_context() context = the_input._get_render_context()
...@@ -381,6 +380,31 @@ class MatlabTest(unittest.TestCase): ...@@ -381,6 +380,31 @@ class MatlabTest(unittest.TestCase):
self.assertEqual(context, expected) self.assertEqual(context, expected)
def test_rendering_while_queued(self):
state = {'value': 'print "good evening"',
'status': 'incomplete',
'input_state': {'queuestate': 'queued'},
}
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': self.input_class.plot_submitted_msg,
'mode': self.mode,
'rows': self.rows,
'cols': self.cols,
'queue_msg': '',
'linenumbers': 'true',
'hidden': '',
'tabsize': int(self.tabsize),
'queue_len': '1',
}
self.assertEqual(context, expected)
def test_plot_data(self): def test_plot_data(self):
get = {'submission': 'x = 1234;'} get = {'submission': 'x = 1234;'}
response = self.the_input.handle_ajax("plot", get) response = self.the_input.handle_ajax("plot", get)
...@@ -391,6 +415,43 @@ class MatlabTest(unittest.TestCase): ...@@ -391,6 +415,43 @@ class MatlabTest(unittest.TestCase):
self.assertTrue(self.the_input.input_state['queuekey'] is not None) self.assertTrue(self.the_input.input_state['queuekey'] is not None)
self.assertEqual(self.the_input.input_state['queuestate'], 'queued') self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
def test_ungraded_response_success(self):
queuekey = 'abcd'
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
state = {'value': 'print "good evening"',
'status': 'incomplete',
'input_state': input_state,
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
the_input.ungraded_response(queue_msg, queuekey)
self.assertTrue(input_state['queuekey'] is None)
self.assertTrue(input_state['queuestate'] is None)
self.assertEqual(input_state['queue_msg'], inner_msg)
def test_ungraded_response_key_mismatch(self):
queuekey = 'abcd'
input_state = {'queuekey': queuekey, 'queuestate': 'queued'}
state = {'value': 'print "good evening"',
'status': 'incomplete',
'input_state': input_state,
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
the_input.ungraded_response(queue_msg, 'abc')
self.assertEqual(input_state['queuekey'], queuekey)
self.assertEqual(input_state['queuestate'], 'queued')
self.assertFalse('queue_msg' in input_state)
......
...@@ -13,6 +13,8 @@ import textwrap ...@@ -13,6 +13,8 @@ import textwrap
from . import test_system from . import test_system
import capa.capa_problem as lcp import capa.capa_problem as lcp
from capa.responsetypes import LoncapaProblemError, \
StudentInputError, ResponseError
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat from capa.xqueue_interface import dateformat
...@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest): ...@@ -864,7 +866,7 @@ class CustomResponseTest(ResponseTest):
# Message is interpreted as an "overall message" # Message is interpreted as an "overall message"
self.assertEqual(correct_map.get_overall_message(), 'Message text') self.assertEqual(correct_map.get_overall_message(), 'Message text')
def test_script_exception(self): def test_script_exception_function(self):
# Construct a script that will raise an exception # Construct a script that will raise an exception
script = textwrap.dedent(""" script = textwrap.dedent("""
...@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest): ...@@ -875,7 +877,17 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'})
def test_script_exception_inline(self):
# Construct a script that will raise an exception
script = 'raise Exception("Test")'
problem = self.build_problem(answer=script)
# Expect that an exception gets raised when we check the answer
with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_invalid_dict_exception(self): def test_invalid_dict_exception(self):
...@@ -889,10 +901,70 @@ class CustomResponseTest(ResponseTest): ...@@ -889,10 +901,70 @@ class CustomResponseTest(ResponseTest):
problem = self.build_problem(script=script, cfn="check_func") problem = self.build_problem(script=script, cfn="check_func")
# Expect that an exception gets raised when we check the answer # Expect that an exception gets raised when we check the answer
with self.assertRaises(Exception): with self.assertRaises(ResponseError):
problem.grade_answers({'1_2_1': '42'}) problem.grade_answers({'1_2_1': '42'})
def test_module_imports_inline(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
# Create a script that checks that the name is defined
# If the name is not defined, then the script
# will raise an exception
script = textwrap.dedent('''
correct[0] = 'correct'
assert('%s' in globals())''' % module_name)
# Create the problem
problem = self.build_problem(answer=script)
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
def test_module_imports_function(self):
'''
Check that the correct modules are available to custom
response scripts
'''
for module_name in ['random', 'numpy', 'math', 'scipy',
'calc', 'eia', 'chemcalc', 'chemtools',
'miller', 'draganddrop']:
# Create a script that checks that the name is defined
# If the name is not defined, then the script
# will raise an exception
script = textwrap.dedent('''
def check_func(expect, answer_given):
assert('%s' in globals())
return True''' % module_name)
# Create the problem
problem = self.build_problem(script=script, cfn="check_func")
# Expect that we can grade an answer without
# getting an exception
try:
problem.grade_answers({'1_2_1': '42'})
except ResponseError:
self.fail("Could not use name '%s' in custom response"
% module_name)
class SchematicResponseTest(ResponseTest): class SchematicResponseTest(ResponseTest):
from response_xml_factory import SchematicResponseXMLFactory from response_xml_factory import SchematicResponseXMLFactory
xml_factory_class = SchematicResponseXMLFactory xml_factory_class = SchematicResponseXMLFactory
...@@ -922,6 +994,18 @@ class SchematicResponseTest(ResponseTest): ...@@ -922,6 +994,18 @@ class SchematicResponseTest(ResponseTest):
# is what we expect) # is what we expect)
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct') self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
def test_script_exception(self):
# Construct a script that will raise an exception
script = "raise Exception('test')"
problem = self.build_problem(answer=script)
# Expect that an exception gets raised when we check the answer
with self.assertRaises(ResponseError):
submission_dict = {'test': 'test'}
input_dict = {'1_2_1': json.dumps(submission_dict)}
problem.grade_answers(input_dict)
class AnnotationResponseTest(ResponseTest): class AnnotationResponseTest(ResponseTest):
from response_xml_factory import AnnotationResponseXMLFactory from response_xml_factory import AnnotationResponseXMLFactory
......
...@@ -33,7 +33,7 @@ def group_from_value(groups, v): ...@@ -33,7 +33,7 @@ def group_from_value(groups, v):
class ABTestFields(object): class ABTestFields(object):
group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content) group_portions = Object(help="What proportions of students should go in each group", default={DEFAULT: 1}, scope=Scope.content)
group_assignments = Object(help="What group this user belongs to", scope=Scope.student_preferences, default={}) group_assignments = Object(help="What group this user belongs to", scope=Scope.preferences, default={})
group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []}) group_content = Object(help="What content to display to each group", scope=Scope.content, default={DEFAULT: []})
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content) experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
has_children = True has_children = True
......
...@@ -20,8 +20,7 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -20,8 +20,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee'), resource_string(__name__, 'js/src/html/display.coffee'),
resource_string(__name__, 'js/src/annotatable/display.coffee')], resource_string(__name__, 'js/src/annotatable/display.coffee')],
'js': [] 'js': []}
}
js_module_name = "Annotatable" js_module_name = "Annotatable"
css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]} css = {'scss': [resource_string(__name__, 'css/annotatable/display.scss')]}
icon_class = 'annotatable' icon_class = 'annotatable'
...@@ -49,11 +48,11 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -49,11 +48,11 @@ class AnnotatableModule(AnnotatableFields, XModule):
if color is not None: if color is not None:
if color in self.highlight_colors: if color in self.highlight_colors:
cls.append('highlight-'+color) cls.append('highlight-' + color)
attr['_delete'] = highlight_key attr['_delete'] = highlight_key
attr['value'] = ' '.join(cls) attr['value'] = ' '.join(cls)
return { 'class' : attr } return {'class': attr}
def _get_annotation_data_attr(self, index, el): def _get_annotation_data_attr(self, index, el):
""" Returns a dict in which the keys are the HTML data attributes """ Returns a dict in which the keys are the HTML data attributes
...@@ -73,7 +72,7 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -73,7 +72,7 @@ class AnnotatableModule(AnnotatableFields, XModule):
if xml_key in el.attrib: if xml_key in el.attrib:
value = el.get(xml_key, '') value = el.get(xml_key, '')
html_key = attrs_map[xml_key] html_key = attrs_map[xml_key]
data_attrs[html_key] = { 'value': value, '_delete': xml_key } data_attrs[html_key] = {'value': value, '_delete': xml_key}
return data_attrs return data_attrs
...@@ -91,7 +90,6 @@ class AnnotatableModule(AnnotatableFields, XModule): ...@@ -91,7 +90,6 @@ class AnnotatableModule(AnnotatableFields, XModule):
delete_key = attr[key]['_delete'] delete_key = attr[key]['_delete']
del el.attrib[delete_key] del el.attrib[delete_key]
def _render_content(self): def _render_content(self):
""" Renders annotatable content with annotation spans and returns HTML. """ """ Renders annotatable content with annotation spans and returns HTML. """
xmltree = etree.fromstring(self.content) xmltree = etree.fromstring(self.content)
...@@ -132,4 +130,3 @@ class AnnotatableDescriptor(AnnotatableFields, RawDescriptor): ...@@ -132,4 +130,3 @@ class AnnotatableDescriptor(AnnotatableFields, RawDescriptor):
stores_state = True stores_state = True
template_dir_name = "annotatable" template_dir_name = "annotatable"
mako_template = "widgets/raw-edit.html" mako_template = "widgets/raw-edit.html"
import json
import logging import logging
from lxml import etree from lxml import etree
...@@ -6,14 +5,16 @@ from pkg_resources import resource_string ...@@ -6,14 +5,16 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List from xblock.core import Integer, Scope, String, Boolean, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload", V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
"skip_spelling_checks", "due", "graceperiod", "max_score"] "skip_spelling_checks", "due", "graceperiod"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state", V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
"student_attempts", "ready_to_reset"] "student_attempts", "ready_to_reset"]
...@@ -49,26 +50,26 @@ class VersionInteger(Integer): ...@@ -49,26 +50,26 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object): class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings) display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.student_state) current_task_number = Integer(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state) task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
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.student_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.student_state) scope=Scope.user_state)
ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False,
scope=Scope.student_state) scope=Scope.user_state)
attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings) attempts = Integer(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings) is_graded = Boolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False,
scope=Scope.settings) scope=Scope.settings)
skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
scope=Scope.settings) scope=Scope.settings)
due = String(help="Date that this problem is due by", default=None, scope=Scope.settings) due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None, graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
scope=Scope.settings) scope=Scope.settings)
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
...@@ -104,10 +105,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -104,10 +105,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
icon_class = 'problem' icon_class = 'problem'
js = {'coffee': [resource_string(__name__, 'js/src/combinedopenended/display.coffee'), js = {'coffee':
resource_string(__name__, 'js/src/collapsible.coffee'), [resource_string(__name__, 'js/src/combinedopenended/display.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
]} resource_string(__name__, 'js/src/javascript_loader.coffee'),
]}
js_module_name = "CombinedOpenEnded" js_module_name = "CombinedOpenEnded"
css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]} css = {'scss': [resource_string(__name__, 'css/combinedopenended/display.scss')]}
...@@ -118,7 +120,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -118,7 +120,7 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -190,8 +192,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule): ...@@ -190,8 +192,8 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
def get_score(self): def get_score(self):
return self.child_module.get_score() return self.child_module.get_score()
#def max_score(self): def max_score(self):
# return self.child_module.max_score() return self.child_module.max_score()
def get_progress(self): def get_progress(self):
return self.child_module.get_progress() return self.child_module.get_progress()
...@@ -217,5 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): ...@@ -217,5 +219,5 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
stores_state = True stores_state = True
has_score = True has_score = True
always_recalculate_grades=True
template_dir_name = "combinedopenended" template_dir_name = "combinedopenended"
...@@ -125,7 +125,8 @@ class ConditionalModule(ConditionalFields, XModule): ...@@ -125,7 +125,8 @@ class ConditionalModule(ConditionalFields, XModule):
an AJAX call. an AJAX call.
""" """
if not self.is_condition_satisfied(): if not self.is_condition_satisfied():
message = self.descriptor.xml_attributes.get('message') defmsg = "{link} must be attempted before this will become visible."
message = self.descriptor.xml_attributes.get('message', defmsg)
context = {'module': self, context = {'module': self,
'message': message} 'message': message}
html = self.system.render_template('conditional_module.html', html = self.system.render_template('conditional_module.html',
......
...@@ -14,6 +14,7 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule ...@@ -14,6 +14,7 @@ from xmodule.seq_module import SequenceDescriptor, SequenceModule
from xmodule.timeparse import parse_time from xmodule.timeparse import parse_time
from xmodule.util.decorators import lazyproperty from xmodule.util.decorators import lazyproperty
from xmodule.graders import grader_from_conf from xmodule.graders import grader_from_conf
from xmodule.util.date_utils import time_to_datetime
import json import json
from xblock.core import Scope, List, String, Object, Boolean from xblock.core import Scope, List, String, Object, Boolean
...@@ -533,19 +534,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -533,19 +534,17 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
def _sorting_dates(self): def _sorting_dates(self):
# utility function to get datetime objects for dates used to # utility function to get datetime objects for dates used to
# compute the is_new flag and the sorting_score # compute the is_new flag and the sorting_score
def to_datetime(timestamp):
return datetime(*timestamp[:6])
announcement = self.announcement announcement = self.announcement
if announcement is not None: if announcement is not None:
announcement = to_datetime(announcement) announcement = time_to_datetime(announcement)
try: try:
start = dateutil.parser.parse(self.advertised_start) start = dateutil.parser.parse(self.advertised_start)
except (ValueError, AttributeError): except (ValueError, AttributeError):
start = to_datetime(self.start) start = time_to_datetime(self.start)
now = to_datetime(time.gmtime()) now = datetime.utcnow()
return announcement, start, now return announcement, start, now
...@@ -653,7 +652,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor): ...@@ -653,7 +652,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
@property @property
def end_date_text(self): def end_date_text(self):
return time.strftime("%b %d, %Y", self.end) """
Returns the end date for the course formatted as a string.
If the course does not have an end date set (course.end is None), an empty string will be returned.
"""
return '' if self.end is None else time.strftime("%b %d, %Y", self.end)
@property @property
def forum_posts_allowed(self): def forum_posts_allowed(self):
......
/* TODO: move top-level variables to a common _variables.scss.
* NOTE: These variables were only added here because when this was integrated with the CMS,
* SASS compilation errors were triggered because the CMS didn't have the same variables defined
* that the LMS did, so the quick fix was to localize the LMS variables not shared by the CMS.
* -Abarrett and Vshnayder
*/
$border-color: #C8C8C8; $border-color: #C8C8C8;
$body-font-size: em(14); $body-font-size: em(14);
.annotatable-wrapper {
position: relative;
}
.annotatable-header { .annotatable-header {
margin-bottom: .5em; margin-bottom: .5em;
.annotatable-title { .annotatable-title {
...@@ -55,6 +65,7 @@ $body-font-size: em(14); ...@@ -55,6 +65,7 @@ $body-font-size: em(14);
display: inline; display: inline;
cursor: pointer; cursor: pointer;
$highlight_index: 0;
@each $highlight in ( @each $highlight in (
(yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)), (yellow rgba(255,255,10,0.3) rgba(255,255,10,0.9)),
(red rgba(178,19,16,0.3) rgba(178,19,16,0.9)), (red rgba(178,19,16,0.3) rgba(178,19,16,0.9)),
...@@ -62,12 +73,13 @@ $body-font-size: em(14); ...@@ -62,12 +73,13 @@ $body-font-size: em(14);
(green rgba(25,255,132,0.3) rgba(25,255,132,0.9)), (green rgba(25,255,132,0.3) rgba(25,255,132,0.9)),
(blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)), (blue rgba(35,163,255,0.3) rgba(35,163,255,0.9)),
(purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) { (purple rgba(115,9,178,0.3) rgba(115,9,178,0.9))) {
$highlight_index: $highlight_index + 1;
$marker: nth($highlight,1); $marker: nth($highlight,1);
$color: nth($highlight,2); $color: nth($highlight,2);
$selected_color: nth($highlight,3); $selected_color: nth($highlight,3);
@if $marker == yellow { @if $highlight_index == 1 {
&.highlight { &.highlight {
background-color: $color; background-color: $color;
&.selected { background-color: $selected_color; } &.selected { background-color: $selected_color; }
...@@ -127,6 +139,7 @@ $body-font-size: em(14); ...@@ -127,6 +139,7 @@ $body-font-size: em(14);
font-weight: 400; font-weight: 400;
padding: 0 10px 10px 10px; padding: 0 10px 10px 10px;
background-color: transparent; background-color: transparent;
border-color: transparent;
} }
p { p {
color: inherit; color: inherit;
...@@ -143,6 +156,7 @@ $body-font-size: em(14); ...@@ -143,6 +156,7 @@ $body-font-size: em(14);
margin: 0px 0px 10px 0; margin: 0px 0px 10px 0;
max-height: 225px; max-height: 225px;
overflow: auto; overflow: auto;
line-height: normal;
} }
.annotatable-reply { .annotatable-reply {
display: block; display: block;
...@@ -165,5 +179,3 @@ $body-font-size: em(14); ...@@ -165,5 +179,3 @@ $body-font-size: em(14);
border-top-color: rgba(0, 0, 0, .85); border-top-color: rgba(0, 0, 0, .85);
} }
} }
class InvalidDefinitionError(Exception): class InvalidDefinitionError(Exception):
pass pass
class NotFoundError(Exception): class NotFoundError(Exception):
pass pass
class ProcessingError(Exception):
'''
An error occurred while processing a request to the XModule.
For example: if an exception occurs while checking a capa problem.
'''
pass
import logging import logging
from lxml import etree from lxml import etree
from dateutil import parser
from pkg_resources import resource_string from pkg_resources import resource_string
...@@ -8,6 +7,9 @@ from xmodule.editing_module import EditingDescriptor ...@@ -8,6 +7,9 @@ from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xblock.core import Scope, Integer, String from xblock.core import Scope, Integer, String
from .fields import Date
from xmodule.util.date_utils import time_to_datetime
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -16,7 +18,7 @@ class FolditFields(object): ...@@ -16,7 +18,7 @@ class FolditFields(object):
# default to what Spring_7012x uses # default to what Spring_7012x uses
required_level = Integer(default=4, scope=Scope.settings) required_level = Integer(default=4, scope=Scope.settings)
required_sublevel = Integer(default=5, scope=Scope.settings) required_sublevel = Integer(default=5, scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings, default='') due = Date(help="Date that this problem is due by", scope=Scope.settings)
show_basic_score = String(scope=Scope.settings, default='false') show_basic_score = String(scope=Scope.settings, default='false')
show_leaderboard = String(scope=Scope.settings, default='false') show_leaderboard = String(scope=Scope.settings, default='false')
...@@ -36,17 +38,8 @@ class FolditModule(FolditFields, XModule): ...@@ -36,17 +38,8 @@ class FolditModule(FolditFields, XModule):
required_sublevel="3" required_sublevel="3"
show_leaderboard="false"/> show_leaderboard="false"/>
""" """
def parse_due_date():
"""
Pull out the date, or None
"""
s = self.due
if s:
return parser.parse(s)
else:
return None
self.due_time = parse_due_date() self.due_time = time_to_datetime(self.due)
def is_complete(self): def is_complete(self):
""" """
...@@ -178,8 +171,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor): ...@@ -178,8 +171,8 @@ class FolditDescriptor(FolditFields, XmlDescriptor, EditingDescriptor):
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
return ({}, []) return {}, []
def definition_to_xml(self): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('foldit') xml_object = etree.Element('foldit')
return xml_object return xml_object
class @Annotatable class @Annotatable
_debug: false _debug: false
# selectors for the annotatable xmodule # selectors for the annotatable xmodule
wrapperSelector: '.annotatable-wrapper'
toggleAnnotationsSelector: '.annotatable-toggle-annotations' toggleAnnotationsSelector: '.annotatable-toggle-annotations'
toggleInstructionsSelector: '.annotatable-toggle-instructions' toggleInstructionsSelector: '.annotatable-toggle-instructions'
instructionsSelector: '.annotatable-instructions' instructionsSelector: '.annotatable-instructions'
...@@ -61,7 +62,7 @@ class @Annotatable ...@@ -61,7 +62,7 @@ class @Annotatable
my: 'bottom center' # of tooltip my: 'bottom center' # of tooltip
at: 'top center' # of target at: 'top center' # of target
target: $(el) # where the tooltip was triggered (i.e. the annotation span) target: $(el) # where the tooltip was triggered (i.e. the annotation span)
container: @$el container: @$(@wrapperSelector)
adjust: adjust:
y: -5 y: -5
show: show:
...@@ -75,6 +76,7 @@ class @Annotatable ...@@ -75,6 +76,7 @@ class @Annotatable
classes: 'ui-tooltip-annotatable' classes: 'ui-tooltip-annotatable'
events: events:
show: @onShowTip show: @onShowTip
move: @onMoveTip
onClickToggleAnnotations: (e) => @toggleAnnotations() onClickToggleAnnotations: (e) => @toggleAnnotations()
...@@ -87,6 +89,55 @@ class @Annotatable ...@@ -87,6 +89,55 @@ class @Annotatable
onShowTip: (event, api) => onShowTip: (event, api) =>
event.preventDefault() if @annotationsHidden event.preventDefault() if @annotationsHidden
onMoveTip: (event, api, position) =>
###
This method handles an edge case in which a tooltip is displayed above
a non-overlapping span like this:
(( TOOLTIP ))
\/
text text text ... text text text ...... <span span span>
<span span span>
The problem is that the tooltip looks disconnected from both spans, so
we should re-position the tooltip to appear above the span.
###
tip = api.elements.tooltip
adjust_y = api.options.position?.adjust?.y || 0
container = api.options.position?.container || $('body')
target = api.elements.target
rects = $(target).get(0).getClientRects()
is_non_overlapping = (rects?.length == 2 and rects[0].left > rects[1].right)
if is_non_overlapping
# we want to choose the largest of the two non-overlapping spans and display
# the tooltip above the center of it (see api.options.position settings)
focus_rect = (if rects[0].width > rects[1].width then rects[0] else rects[1])
rect_center = focus_rect.left + (focus_rect.width / 2)
rect_top = focus_rect.top
tip_width = $(tip).width()
tip_height = $(tip).height()
# tooltip is positioned relative to its container, so we need to factor in offsets
container_offset = $(container).offset()
offset_left = -container_offset.left
offset_top = $(document).scrollTop() - container_offset.top
tip_left = offset_left + rect_center - (tip_width / 2)
tip_top = offset_top + rect_top - tip_height + adjust_y
# make sure the new tip position doesn't clip the edges of the screen
win_width = $(window).width()
if tip_left < offset_left
tip_left = offset_left
else if tip_left + tip_width > win_width + offset_left
tip_left = win_width + offset_left - tip_width
# final step: update the position object (used by qtip2 to show the tip after the move event)
$.extend position, 'left': tip_left, 'top': tip_top
getSpanForProblemReturn: (el) -> getSpanForProblemReturn: (el) ->
problem_id = $(@problemReturnSelector).index(el) problem_id = $(@problemReturnSelector).index(el)
@$(@spanSelector).filter("[data-problem-id='#{problem_id}']") @$(@spanSelector).filter("[data-problem-id='#{problem_id}']")
......
...@@ -10,6 +10,7 @@ from collections import namedtuple ...@@ -10,6 +10,7 @@ from collections import namedtuple
from .exceptions import InvalidLocationError, InsufficientSpecificationError from .exceptions import InvalidLocationError, InsufficientSpecificationError
from xmodule.errortracker import ErrorLog, make_error_tracker from xmodule.errortracker import ErrorLog, make_error_tracker
from bson.son import SON
log = logging.getLogger('mitx.' + 'modulestore') log = logging.getLogger('mitx.' + 'modulestore')
...@@ -457,3 +458,13 @@ class ModuleStoreBase(ModuleStore): ...@@ -457,3 +458,13 @@ class ModuleStoreBase(ModuleStore):
if c.id == course_id: if c.id == course_id:
return c return c
return None return None
def namedtuple_to_son(namedtuple, prefix=''):
"""
Converts a namedtuple into a SON object with the same key order
"""
son = SON()
for idx, field_name in enumerate(namedtuple._fields):
son[prefix + field_name] = namedtuple[idx]
return son
from datetime import datetime from datetime import datetime
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location, namedtuple_to_son
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
import logging
DRAFT = 'draft' DRAFT = 'draft'
...@@ -15,11 +16,11 @@ def as_draft(location): ...@@ -15,11 +16,11 @@ def as_draft(location):
def wrap_draft(item): def wrap_draft(item):
""" """
Sets `item.cms.is_draft` to `True` if the item is a Sets `item.is_draft` to `True` if the item is a
draft, and `False` otherwise. Sets the item's location to the draft, and `False` otherwise. Sets the item's location to the
non-draft location in either case non-draft location in either case
""" """
item.cms.is_draft = item.location.revision == DRAFT setattr(item, 'is_draft', item.location.revision == DRAFT)
item.location = item.location._replace(revision=None) item.location = item.location._replace(revision=None)
return item return item
...@@ -55,11 +56,10 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -55,11 +56,10 @@ class DraftModuleStore(ModuleStoreBase):
get_children() to cache. None indicates to cache all descendents get_children() to cache. None indicates to cache all descendents
""" """
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
try: try:
return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth=0)) return wrap_draft(super(DraftModuleStore, self).get_item(as_draft(location), depth=depth))
except ItemNotFoundError: except ItemNotFoundError:
return wrap_draft(super(DraftModuleStore, self).get_item(location, depth=0)) return wrap_draft(super(DraftModuleStore, self).get_item(location, depth=depth))
def get_instance(self, course_id, location, depth=0): def get_instance(self, course_id, location, depth=0):
""" """
...@@ -67,11 +67,10 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -67,11 +67,10 @@ class DraftModuleStore(ModuleStoreBase):
TODO (vshnayder): this may want to live outside the modulestore eventually TODO (vshnayder): this may want to live outside the modulestore eventually
""" """
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well
try: try:
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=0)) return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, as_draft(location), depth=depth))
except ItemNotFoundError: except ItemNotFoundError:
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=0)) return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=depth))
def get_items(self, location, course_id=None, depth=0): def get_items(self, location, course_id=None, depth=0):
""" """
...@@ -88,9 +87,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -88,9 +87,8 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
# cdodge: we're forcing depth=0 here as the Draft store is not handling caching well draft_items = super(DraftModuleStore, self).get_items(draft_loc, course_id=course_id, depth=depth)
draft_items = super(DraftModuleStore, self).get_items(draft_loc, course_id=course_id, depth=0) items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=depth)
items = super(DraftModuleStore, self).get_items(location, course_id=course_id, depth=0)
draft_locs_found = set(item.location._replace(revision=None) for item in draft_items) draft_locs_found = set(item.location._replace(revision=None) for item in draft_items)
non_draft_items = [ non_draft_items = [
...@@ -118,7 +116,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -118,7 +116,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.cms.is_draft: if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_item(draft_loc, data) return super(DraftModuleStore, self).update_item(draft_loc, data)
...@@ -133,7 +131,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -133,7 +131,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.cms.is_draft: if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_children(draft_loc, children) return super(DraftModuleStore, self).update_children(draft_loc, children)
...@@ -149,7 +147,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -149,7 +147,7 @@ class DraftModuleStore(ModuleStoreBase):
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) draft_item = self.get_item(location)
if not draft_item.cms.is_draft: if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc) self.clone_item(location, draft_loc)
if 'is_draft' in metadata: if 'is_draft' in metadata:
...@@ -192,3 +190,36 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -192,3 +190,36 @@ class DraftModuleStore(ModuleStoreBase):
""" """
super(DraftModuleStore, self).clone_item(location, as_draft(location)) super(DraftModuleStore, self).clone_item(location, as_draft(location))
super(DraftModuleStore, self).delete_item(location) super(DraftModuleStore, self).delete_item(location)
def _query_children_for_cache_children(self, items):
# first get non-draft in a round-trip
queried_children = []
to_process_non_drafts = super(DraftModuleStore, self)._query_children_for_cache_children(items)
to_process_dict = {}
for non_draft in to_process_non_drafts:
to_process_dict[Location(non_draft["_id"])] = non_draft
# now query all draft content in another round-trip
query = {
'_id': {'$in': [namedtuple_to_son(as_draft(Location(item))) for item in items]}
}
to_process_drafts = list(self.collection.find(query))
# now we have to go through all drafts and replace the non-draft
# with the draft. This is because the semantics of the DraftStore is to
# always return the draft - if available
for draft in to_process_drafts:
draft_loc = Location(draft["_id"])
draft_as_non_draft_loc = draft_loc._replace(revision=None)
# does non-draft exist in the collection
# if so, replace it
if draft_as_non_draft_loc in to_process_dict:
to_process_dict[draft_as_non_draft_loc] = draft
# convert the dict - which is used for look ups - back into a list
for key, value in to_process_dict.iteritems():
queried_children.append(value)
return queried_children
...@@ -3,7 +3,6 @@ import sys ...@@ -3,7 +3,6 @@ import sys
import logging import logging
import copy import copy
from bson.son import SON
from collections import namedtuple from collections import namedtuple
from fs.osfs import OSFS from fs.osfs import OSFS
from itertools import repeat from itertools import repeat
...@@ -19,7 +18,7 @@ from xmodule.error_module import ErrorDescriptor ...@@ -19,7 +18,7 @@ from xmodule.error_module import ErrorDescriptor
from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError from xblock.runtime import DbModel, KeyValueStore, InvalidScopeError
from xblock.core import Scope from xblock.core import Scope
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location, namedtuple_to_son
from .draft import DraftModuleStore from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
DuplicateItemError) DuplicateItemError)
...@@ -202,16 +201,6 @@ def location_to_query(location, wildcard=True): ...@@ -202,16 +201,6 @@ def location_to_query(location, wildcard=True):
return query return query
def namedtuple_to_son(ntuple, prefix=''):
"""
Converts a namedtuple into a SON object with the same key order
"""
son = SON()
for idx, field_name in enumerate(ntuple._fields):
son[prefix + field_name] = ntuple[idx]
return son
metadata_cache_key = attrgetter('org', 'course') metadata_cache_key = attrgetter('org', 'course')
...@@ -383,6 +372,13 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -383,6 +372,13 @@ class MongoModuleStore(ModuleStoreBase):
item['location'] = item['_id'] item['location'] = item['_id']
del item['_id'] del item['_id']
def _query_children_for_cache_children(self, items):
# first get non-draft in a round-trip
query = {
'_id': {'$in': [namedtuple_to_son(Location(item)) for item in items]}
}
return list(self.collection.find(query))
def _cache_children(self, items, depth=0): def _cache_children(self, items, depth=0):
""" """
Returns a dictionary mapping Location -> item data, populated with json data Returns a dictionary mapping Location -> item data, populated with json data
...@@ -407,13 +403,10 @@ class MongoModuleStore(ModuleStoreBase): ...@@ -407,13 +403,10 @@ class MongoModuleStore(ModuleStoreBase):
# Load all children by id. See # Load all children by id. See
# http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or # http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or
# for or-query syntax # for or-query syntax
to_process = []
if children: if children:
query = { to_process = self._query_children_for_cache_children(children)
'_id': {'$in': [namedtuple_to_son(Location(child)) for child in children]}
}
to_process = self.collection.find(query)
else:
to_process = []
# If depth is None, then we just recurse until we hit all the descendents # If depth is None, then we just recurse until we hit all the descendents
if depth is not None: if depth is not None:
depth -= 1 depth -= 1
......
...@@ -136,3 +136,4 @@ def delete_course(modulestore, contentstore, source_location, commit = False): ...@@ -136,3 +136,4 @@ def delete_course(modulestore, contentstore, source_location, commit = False):
modulestore.delete_item(source_location) modulestore.delete_item(source_location)
return True return True
...@@ -356,6 +356,48 @@ def remap_namespace(module, target_location_namespace): ...@@ -356,6 +356,48 @@ def remap_namespace(module, target_location_namespace):
return module return module
def allowed_metadata_by_category(category):
# should this be in the descriptors?!?
return {
'vertical': [],
'chapter': ['start'],
'sequential': ['due', 'format', 'start', 'graded']
}.get(category,['*'])
def check_module_metadata_editability(module):
'''
Assert that there is no metadata within a particular module that we can't support editing
However we always allow 'display_name' and 'xml_attribtues'
'''
allowed = allowed_metadata_by_category(module.location.category)
if '*' in allowed:
# everything is allowed
return 0
allowed = allowed + ['xml_attributes', 'display_name']
err_cnt = 0
my_metadata = dict(own_metadata(module))
illegal_keys = set(own_metadata(module).keys()) - set(allowed)
if len(illegal_keys) > 0:
err_cnt = err_cnt + 1
print ': found non-editable metadata on {0}. These metadata keys are not supported = {1}'. format(module.location.url(), illegal_keys)
return err_cnt
def validate_no_non_editable_metadata(module_store, course_id, category):
err_cnt = 0
for module_loc in module_store.modules[course_id]:
module = module_store.modules[course_id][module_loc]
if module.location.category == category:
err_cnt = err_cnt + check_module_metadata_editability(module)
return err_cnt
def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category): def validate_category_hierarchy(module_store, course_id, parent_category, expected_child_category):
err_cnt = 0 err_cnt = 0
...@@ -440,6 +482,12 @@ def perform_xlint(data_dir, course_dirs, ...@@ -440,6 +482,12 @@ def perform_xlint(data_dir, course_dirs,
err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential") err_cnt += validate_category_hierarchy(module_store, course_id, "chapter", "sequential")
# constrain that sequentials only have 'verticals' # constrain that sequentials only have 'verticals'
err_cnt += validate_category_hierarchy(module_store, course_id, "sequential", "vertical") err_cnt += validate_category_hierarchy(module_store, course_id, "sequential", "vertical")
# don't allow metadata on verticals, since we can't edit them in studio
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "vertical")
# don't allow metadata on chapters, since we can't edit them in studio
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "chapter")
# don't allow metadata on sequences that we can't edit
err_cnt += validate_no_non_editable_metadata(module_store, course_id, "sequential")
# check for a presence of a course marketing video # check for a presence of a course marketing video
location_elements = course_id.split('/') location_elements = course_id.split('/')
...@@ -456,3 +504,5 @@ def perform_xlint(data_dir, course_dirs, ...@@ -456,3 +504,5 @@ def perform_xlint(data_dir, course_dirs,
print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing" print "This course can be imported, but some errors may occur during the run of the course. It is recommend that you fix your courseware before importing"
else: else:
print "This course can be imported successfully." print "This course can be imported successfully."
return err_cnt
...@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware") ...@@ -19,10 +19,6 @@ log = logging.getLogger("mitx.courseware")
# attempts specified in xml definition overrides this. # attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1 MAX_ATTEMPTS = 1
# Set maximum available number of points.
# Overriden by max_score specified in xml.
MAX_SCORE = 1
#The highest score allowed for the overall xmodule and for each rubric point #The highest score allowed for the overall xmodule and for each rubric point
MAX_SCORE_ALLOWED = 50 MAX_SCORE_ALLOWED = 50
...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module(): ...@@ -88,7 +84,7 @@ class CombinedOpenEndedV1Module():
Definition file should have one or many task blocks, a rubric block, and a prompt block: Definition file should have one or many task blocks, a rubric block, and a prompt block:
Sample file: Sample file:
<combinedopenended attempts="10000" max_score="1"> <combinedopenended attempts="10000">
<rubric> <rubric>
Blah blah rubric. Blah blah rubric.
</rubric> </rubric>
...@@ -143,23 +139,19 @@ class CombinedOpenEndedV1Module(): ...@@ -143,23 +139,19 @@ class CombinedOpenEndedV1Module():
self.accept_file_upload = self.instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT self.accept_file_upload = self.instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT
self.skip_basic_checks = self.instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT self.skip_basic_checks = self.instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT
display_due_date_string = self.instance_state.get('due', None) due_date = self.instance_state.get('due', None)
grace_period_string = self.instance_state.get('graceperiod', None) grace_period_string = self.instance_state.get('graceperiod', None)
try: try:
self.timeinfo = TimeInfo(display_due_date_string, grace_period_string) self.timeinfo = TimeInfo(due_date, grace_period_string)
except: except:
log.error("Error parsing due date information in location {0}".format(location)) log.error("Error parsing due date information in location {0}".format(location))
raise raise
self.display_due_date = self.timeinfo.display_due_date self.display_due_date = self.timeinfo.display_due_date
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = self.instance_state.get('max_score', MAX_SCORE)
self.rubric_renderer = CombinedOpenEndedRubric(system, True) self.rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_string = stringify_children(definition['rubric']) rubric_string = stringify_children(definition['rubric'])
self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED, self._max_score) self._max_score = self.rubric_renderer.check_if_rubric_is_parseable(rubric_string, location, MAX_SCORE_ALLOWED)
#Static data is passed to the child modules to render #Static data is passed to the child modules to render
self.static_data = { self.static_data = {
......
...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object): ...@@ -79,7 +79,7 @@ class CombinedOpenEndedRubric(object):
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
return {'success': success, 'html': html, 'rubric_scores': rubric_scores} return {'success': success, 'html': html, 'rubric_scores': rubric_scores}
def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed, max_score): def check_if_rubric_is_parseable(self, rubric_string, location, max_score_allowed):
rubric_dict = self.render_rubric(rubric_string) rubric_dict = self.render_rubric(rubric_string)
success = rubric_dict['success'] success = rubric_dict['success']
rubric_feedback = rubric_dict['html'] rubric_feedback = rubric_dict['html']
...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object): ...@@ -101,12 +101,7 @@ class CombinedOpenEndedRubric(object):
log.error(error_message) log.error(error_message)
raise RubricParsingError(error_message) raise RubricParsingError(error_message)
if int(total) != int(max_score): return int(total)
#This is a staff_facing_error
error_msg = "The max score {0} for problem {1} does not match the total number of points in the rubric {2}. Contact the learning sciences group for assistance.".format(
max_score, location, total)
log.error(error_msg)
raise RubricParsingError(error_msg)
def extract_categories(self, element): def extract_categories(self, element):
''' '''
......
from xblock.core import Integer, Float
class StringyFloat(Float):
"""
A model type that converts from string to floats when reading from json
"""
def from_json(self, value):
try:
return float(value)
except:
return None
...@@ -6,13 +6,13 @@ from lxml import etree ...@@ -6,13 +6,13 @@ from lxml import etree
from datetime import datetime from datetime import datetime
from pkg_resources import resource_string from pkg_resources import resource_string
from .capa_module import ComplexEncoder from .capa_module import ComplexEncoder
from .stringify import stringify_children
from .x_module import XModule from .x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope from xblock.core import Object, Integer, Boolean, String, Scope
from xmodule.open_ended_grading_classes.xblock_field_types import StringyFloat
from xmodule.fields import Date
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
...@@ -28,13 +28,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please ...@@ -28,13 +28,18 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object): class PeerGradingFields(object):
use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.", default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings) use_for_single_location = Boolean(help="Whether to use this for a single location or as a panel.",
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, scope=Scope.settings) default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.",default=IS_GRADED, scope=Scope.settings) link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
display_due_date_string = String(help="Due date that should be displayed.", default=None, scope=Scope.settings) scope=Scope.settings)
is_graded = Boolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings) grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE, scope=Scope.settings) max_grade = Integer(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
student_data_for_location = Object(help="Student data for a given peer grading problem.", default=json.dumps({}),scope=Scope.student_state) scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.",
scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
class PeerGradingModule(PeerGradingFields, XModule): class PeerGradingModule(PeerGradingFields, XModule):
...@@ -72,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -72,7 +77,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
self._model_data['due'] = due_date self._model_data['due'] = due_date
try: try:
self.timeinfo = TimeInfo(self.display_due_date_string, self.grace_period_string) self.timeinfo = TimeInfo(self.due_date, self.grace_period_string)
except: except:
log.error("Error parsing due date information in location {0}".format(location)) log.error("Error parsing due date information in location {0}".format(location))
raise raise
...@@ -572,4 +577,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): ...@@ -572,4 +577,5 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
stores_state = True stores_state = True
has_score = True has_score = True
always_recalculate_grades=True
template_dir_name = "peer_grading" template_dir_name = "peer_grading"
...@@ -30,8 +30,8 @@ class PollFields(object): ...@@ -30,8 +30,8 @@ class PollFields(object):
# Name of poll to use in links to this poll # Name of poll to use in links to this poll
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.student_state, default=False) voted = Boolean(help="Whether this student has voted on the poll", scope=Scope.user_state, default=False)
poll_answer = String(help="Student answer", scope=Scope.student_state, default='') poll_answer = String(help="Student answer", scope=Scope.user_state, default='')
poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content) poll_answers = Object(help="All possible answers for the poll fro other students", scope=Scope.content)
answers = List(help="Poll answers from xml", scope=Scope.content, default=[]) answers = List(help="Poll answers from xml", scope=Scope.content, default=[])
......
...@@ -10,7 +10,7 @@ log = logging.getLogger('mitx.' + __name__) ...@@ -10,7 +10,7 @@ log = logging.getLogger('mitx.' + __name__)
class RandomizeFields(object): class RandomizeFields(object):
choice = Integer(help="Which random child was chosen", scope=Scope.student_state) choice = Integer(help="Which random child was chosen", scope=Scope.user_state)
class RandomizeModule(RandomizeFields, XModule): class RandomizeModule(RandomizeFields, XModule):
......
...@@ -23,7 +23,7 @@ class SequenceFields(object): ...@@ -23,7 +23,7 @@ class SequenceFields(object):
# NOTE: Position is 1-indexed. This is silly, but there are now student # NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix. # positions saved on prod, so it's not easy to fix.
position = Integer(help="Last tab viewed in this sequence", scope=Scope.student_state) position = Integer(help="Last tab viewed in this sequence", scope=Scope.user_state)
class SequenceModule(SequenceFields, XModule): class SequenceModule(SequenceFields, XModule):
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
metadata: metadata:
display_name: Open Ended Response display_name: Open Ended Response
max_attempts: 1 max_attempts: 1
max_score: 1
is_graded: False is_graded: False
version: 1 version: 1
display_name: Open Ended Response display_name: Open Ended Response
skip_spelling_checks: False skip_spelling_checks: False
accept_file_upload: False accept_file_upload: False
weight: ""
data: | data: |
<combinedopenended> <combinedopenended>
<rubric> <rubric>
......
...@@ -6,6 +6,7 @@ metadata: ...@@ -6,6 +6,7 @@ metadata:
link_to_location: None link_to_location: None
is_graded: False is_graded: False
max_grade: 1 max_grade: 1
weight: ""
data: | data: |
<peergrading> <peergrading>
</peergrading> </peergrading>
......
...@@ -5,11 +5,15 @@ import unittest ...@@ -5,11 +5,15 @@ import unittest
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule from xmodule.open_ended_grading_classes.open_ended_module import OpenEndedModule
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
from datetime import datetime from datetime import datetime
import logging
log = logging.getLogger(__name__)
from . import test_system from . import test_system
...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -57,7 +61,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_system = test_system() self.test_system = test_system()
self.openendedchild = OpenEndedChild(self.test_system, self.location, self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
def test_latest_answer_empty(self): def test_latest_answer_empty(self):
...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -183,10 +187,12 @@ class OpenEndedModuleTest(unittest.TestCase):
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message") self.mock_xqueue.send_to_queue.return_value = (None, "Message")
def constructed_callback(dispatch="score_update"): def constructed_callback(dispatch="score_update"):
return dispatch return dispatch
self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback,
'default_queuename': 'testqueue',
'waittime': 1} 'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location, self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -281,7 +287,18 @@ class OpenEndedModuleTest(unittest.TestCase):
class CombinedOpenEndedModuleTest(unittest.TestCase): class CombinedOpenEndedModuleTest(unittest.TestCase):
location = Location(["i4x", "edX", "open_ended", "combinedopenended", location = Location(["i4x", "edX", "open_ended", "combinedopenended",
"SampleQuestion"]) "SampleQuestion"])
definition_template = """
<combinedopenended attempts="10000">
{rubric}
{prompt}
<task>
{task1}
</task>
<task>
{task2}
</task>
</combinedopenended>
"""
prompt = "<prompt>This is a question prompt</prompt>" prompt = "<prompt>This is a question prompt</prompt>"
rubric = '''<rubric><rubric> rubric = '''<rubric><rubric>
<category> <category>
...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -335,10 +352,15 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
</openendedparam> </openendedparam>
</openended>''' </openended>'''
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]} definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
descriptor = Mock() full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
descriptor = Mock(data=full_definition)
test_system = test_system()
combinedoe_container = CombinedOpenEndedModule(test_system,
location,
descriptor,
model_data={'data': full_definition, 'weight' : '1'})
def setUp(self): def setUp(self):
self.test_system = test_system()
# TODO: this constructor call is definitely wrong, but neither branch # TODO: this constructor call is definitely wrong, but neither branch
# of the merge matches the module constructor. Someone (Vik?) should fix this. # of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system, self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -368,3 +390,19 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = self.combinedoe.update_task_states() changed = self.combinedoe.update_task_states()
self.assertTrue(changed) self.assertTrue(changed)
def test_get_max_score(self):
changed = self.combinedoe.update_task_states()
self.combinedoe.state = "done"
self.combinedoe.is_scored = True
max_score = self.combinedoe.max_score()
self.assertEqual(max_score, 1)
def test_container_get_max_score(self):
#The progress view requires that this function be exposed
max_score = self.combinedoe_container.max_score()
self.assertEqual(max_score, None)
def test_container_weight(self):
weight = self.combinedoe_container.weight
self.assertEqual(weight,1)
import unittest import unittest
from time import strptime from time import strptime
import datetime
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from mock import Mock, patch from mock import Mock, patch
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
import xmodule.course_module
from xmodule.util.date_utils import time_to_datetime
ORG = 'test_org' ORG = 'test_org'
...@@ -39,8 +42,19 @@ class DummySystem(ImportSystem): ...@@ -39,8 +42,19 @@ class DummySystem(ImportSystem):
class IsNewCourseTestCase(unittest.TestCase): class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses""" """Make sure the property is_new works on courses"""
def setUp(self):
# Needed for test_is_newish
datetime_patcher = patch.object(
xmodule.course_module, 'datetime',
Mock(wraps=datetime.datetime)
)
mocked_datetime = datetime_patcher.start()
mocked_datetime.utcnow.return_value = time_to_datetime(NOW)
self.addCleanup(datetime_patcher.stop)
@staticmethod @staticmethod
def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None): def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
"""Get a dummy course""" """Get a dummy course"""
system = DummySystem(load_error_modules=True) system = DummySystem(load_error_modules=True)
...@@ -51,6 +65,7 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -51,6 +65,7 @@ class IsNewCourseTestCase(unittest.TestCase):
is_new = to_attrb('is_new', is_new) is_new = to_attrb('is_new', is_new)
announcement = to_attrb('announcement', announcement) announcement = to_attrb('announcement', announcement)
advertised_start = to_attrb('advertised_start', advertised_start) advertised_start = to_attrb('advertised_start', advertised_start)
end = to_attrb('end', end)
start_xml = ''' start_xml = '''
<course org="{org}" course="{course}" <course org="{org}" course="{course}"
...@@ -58,13 +73,14 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -58,13 +73,14 @@ class IsNewCourseTestCase(unittest.TestCase):
start="{start}" start="{start}"
{announcement} {announcement}
{is_new} {is_new}
{advertised_start}> {advertised_start}
{end}>
<chapter url="hi" url_name="ch" display_name="CH"> <chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html> <html url_name="h" display_name="H">Two houses, ...</html>
</chapter> </chapter>
</course> </course>
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new, '''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
announcement=announcement, advertised_start=advertised_start) announcement=announcement, advertised_start=advertised_start, end=end)
return system.process_xml(start_xml) return system.process_xml(start_xml)
...@@ -126,10 +142,7 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -126,10 +142,7 @@ class IsNewCourseTestCase(unittest.TestCase):
print "Checking start=%s advertised=%s" % (s[0], s[1]) print "Checking start=%s advertised=%s" % (s[0], s[1])
self.assertEqual(d.start_date_text, s[2]) self.assertEqual(d.start_date_text, s[2])
@patch('xmodule.course_module.time.gmtime') def test_is_newish(self):
def test_is_newish(self, gmtime_mock):
gmtime_mock.return_value = NOW
descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True) descriptor = self.get_dummy_course(start='2012-12-02T12:00', is_new=True)
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
...@@ -150,3 +163,11 @@ class IsNewCourseTestCase(unittest.TestCase): ...@@ -150,3 +163,11 @@ class IsNewCourseTestCase(unittest.TestCase):
descriptor = self.get_dummy_course(start='2012-12-31T12:00') descriptor = self.get_dummy_course(start='2012-12-31T12:00')
assert(descriptor.is_newish is True) assert(descriptor.is_newish is True)
def test_end_date_text(self):
# No end date set, returns empty string.
d = self.get_dummy_course('2012-12-02T12:00')
self.assertEqual('', d.end_date_text)
d = self.get_dummy_course('2012-12-02T12:00', end='2014-9-04T12:00')
self.assertEqual('Sep 04, 2014', d.end_date_text)
# Tests for xmodule.util.date_utils
from nose.tools import assert_equals
from xmodule.util import date_utils
import datetime
import time
def test_get_time_struct_display():
assert_equals("", date_utils.get_time_struct_display(None, ""))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals("03/12/1992", date_utils.get_time_struct_display(test_time, '%m/%d/%Y'))
assert_equals("15:03", date_utils.get_time_struct_display(test_time, '%H:%M'))
def test_get_default_time_display():
assert_equals("", date_utils.get_default_time_display(None))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals("Mar 12, 1992 at 03:03 PM",
date_utils.get_default_time_display(test_time))
def test_time_to_datetime():
assert_equals(None, date_utils.time_to_datetime(None))
test_time = time.struct_time((1992, 3, 12, 15, 3, 30, 1, 71, 0))
assert_equals(datetime.datetime(1992, 3, 12, 15, 3, 30),
date_utils.time_to_datetime(test_time))
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from path import path
import unittest import unittest
from fs.memoryfs import MemoryFS from fs.memoryfs import MemoryFS
from lxml import etree from lxml import etree
from mock import Mock, patch from mock import Mock, patch
from collections import defaultdict
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
from xmodule.xml_module import is_pointer_tag from xmodule.xml_module import is_pointer_tag
from xmodule.errortracker import make_error_tracker
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.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.inheritance import compute_inherited_metadata from xmodule.modulestore.inheritance import compute_inherited_metadata
from xmodule.fields import Date
from .test_export import DATA_DIR from .test_export import DATA_DIR
...@@ -137,7 +133,7 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -137,7 +133,7 @@ class ImportTestCase(BaseCourseTestCase):
- inherited metadata doesn't leak to children. - inherited metadata doesn't leak to children.
""" """
system = self.get_system() system = self.get_system()
v = '1 hour' v = 'March 20 17:00'
url_name = 'test1' url_name = 'test1'
start_xml = ''' start_xml = '''
<course org="{org}" course="{course}" <course org="{org}" course="{course}"
...@@ -150,11 +146,11 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -150,11 +146,11 @@ class ImportTestCase(BaseCourseTestCase):
compute_inherited_metadata(descriptor) compute_inherited_metadata(descriptor)
print descriptor, descriptor._model_data print descriptor, descriptor._model_data
self.assertEqual(descriptor.lms.due, v) self.assertEqual(descriptor.lms.due, Date().from_json(v))
# Check that the child inherits due correctly # Check that the child inherits due correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.lms.due, v) self.assertEqual(child.lms.due, Date().from_json(v))
# Now export and check things # Now export and check things
resource_fs = MemoryFS() resource_fs = MemoryFS()
......
import dateutil
import dateutil.parser
import datetime
from .timeparse import parse_timedelta from .timeparse import parse_timedelta
from xmodule.util.date_utils import time_to_datetime
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -9,7 +7,7 @@ log = logging.getLogger(__name__) ...@@ -9,7 +7,7 @@ log = logging.getLogger(__name__)
class TimeInfo(object): class TimeInfo(object):
""" """
This is a simple object that calculates and stores datetime information for an XModule This is a simple object that calculates and stores datetime information for an XModule
based on the due date string and the grace period string based on the due date and the grace period string
So far it parses out three different pieces of time information: So far it parses out three different pieces of time information:
self.display_due_date - the 'official' due date that gets displayed to students self.display_due_date - the 'official' due date that gets displayed to students
...@@ -17,13 +15,10 @@ class TimeInfo(object): ...@@ -17,13 +15,10 @@ class TimeInfo(object):
self.close_date - the real due date self.close_date - the real due date
""" """
def __init__(self, display_due_date_string, grace_period_string): def __init__(self, due_date, grace_period_string):
if display_due_date_string is not None: if due_date is not None:
try: self.display_due_date = time_to_datetime(due_date)
self.display_due_date = dateutil.parser.parse(display_due_date_string)
except ValueError:
log.error("Could not parse due date {0}".format(display_due_date_string))
raise
else: else:
self.display_due_date = None self.display_due_date = None
......
...@@ -16,9 +16,9 @@ log = logging.getLogger(__name__) ...@@ -16,9 +16,9 @@ log = logging.getLogger(__name__)
class TimeLimitFields(object): class TimeLimitFields(object):
beginning_at = Float(help="The time this timer was started", scope=Scope.student_state) beginning_at = Float(help="The time this timer was started", scope=Scope.user_state)
ending_at = Float(help="The time this timer will end", scope=Scope.student_state) ending_at = Float(help="The time this timer will end", scope=Scope.user_state)
accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.student_state) accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.user_state)
time_expired_redirect_url = String(help="Url to redirect users to after the timelimit has expired", scope=Scope.settings) time_expired_redirect_url = String(help="Url to redirect users to after the timelimit has expired", scope=Scope.settings)
duration = Float(help="The length of this timer", scope=Scope.settings) duration = Float(help="The length of this timer", scope=Scope.settings)
suppress_toplevel_navigation = Boolean(help="Whether the toplevel navigation should be suppressed when viewing this module", scope=Scope.settings) suppress_toplevel_navigation = Boolean(help="Whether the toplevel navigation should be suppressed when viewing this module", scope=Scope.settings)
......
import time
import datetime
def get_default_time_display(time_struct):
"""
Converts a time struct to a string representation. This is the default
representation used in Studio and LMS.
It is of the form "Apr 09, 2013 at 04:00 PM".
If None is passed in, an empty string will be returned.
"""
return get_time_struct_display(time_struct, "%b %d, %Y at %I:%M %p")
def get_time_struct_display(time_struct, format):
"""
Converts a time struct to a string based on the given format.
If None is passed in, an empty string will be returned.
"""
return '' if time_struct is None else time.strftime(format, time_struct)
def time_to_datetime(time_struct):
"""
Convert a time struct to a datetime.
If None is passed in, None will be returned.
"""
return datetime.datetime(*time_struct[:6]) if time_struct else None
...@@ -19,7 +19,7 @@ log = logging.getLogger(__name__) ...@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
class VideoFields(object): class VideoFields(object):
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.student_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
......
...@@ -21,7 +21,7 @@ log = logging.getLogger(__name__) ...@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
class VideoAlphaFields(object): class VideoAlphaFields(object):
data = String(help="XML data for the problem", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
position = Integer(help="Current position in the video", scope=Scope.student_state, default=0) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
display_name = String(help="Display name for this module", scope=Scope.settings) display_name = String(help="Display name for this module", scope=Scope.settings)
...@@ -131,7 +131,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): ...@@ -131,7 +131,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule):
else: else:
# VS[compat] # VS[compat]
# cdodge: filesystem static content support. # cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(getattr(self, 'data_dir', None)) caption_asset_path = "/static/subs/"
return self.system.render_template('videoalpha.html', { return self.system.render_template('videoalpha.html', {
'youtube_streams': self.youtube_streams, 'youtube_streams': self.youtube_streams,
......
...@@ -340,7 +340,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -340,7 +340,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# cdodge: this is a list of metadata names which are 'system' metadata # cdodge: this is a list of metadata names which are 'system' metadata
# and should not be edited by an end-user # and should not be edited by an end-user
system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft'] system_metadata_fields = ['data_dir', 'published_date', 'published_by', 'is_draft', 'xml_attributes']
# A list of descriptor attributes that must be equal for the descriptors to # A list of descriptor attributes that must be equal for the descriptors to
# be equal # be equal
......
...@@ -12,22 +12,49 @@ ...@@ -12,22 +12,49 @@
} }
} }
//NOTE: // NOTE:
// Genex uses six global functions: // Genex uses 8 global functions, all prefixed with genex:
// genexSetDNASequence (exported from GWT) // 6 are exported from GWT:
// genexSetClickEvent (exported from GWT) // genexSetInitialDNASequence
// genexSetKeyEvent (exported from GWT) // genexSetDNASequence
// genexSetProblemNumber (exported from GWT) // genexGetDNASequence
// genexSetClickEvent
// genexSetKeyEvent
// genexSetProblemNumber
// //
// It calls genexIsReady with a deferred command when it has finished // It calls genexIsReady with a deferred command when it has finished
// initialization and has drawn itself // initialization and has drawn itself
// genexStoreAnswer(answer) is called when the GWT [Store Answer] button // genexStoreAnswer(answer) is called each time the DNA sequence changes
// is clicked // through user interaction
//Genex does not call the following function
genexGetInputField = function() {
var problem = $('#genex_container').parents('.problem');
return problem.find('input[type="hidden"][name!="genex_dna_sequence"][name!="genex_problem_number"]');
};
genexIsReady = function() { genexIsReady = function() {
//Load DNA sequence var input_field = genexGetInputField();
var dna_sequence = $('#dna_sequence').val(); var genex_saved_state = input_field.val();
genexSetDNASequence(dna_sequence); var genex_initial_dna_sequence;
var genex_dna_sequence;
//Get the sequence from xml file
genex_initial_dna_sequence = $('#genex_dna_sequence').val();
//Call this function to set the value used by reset button
genexSetInitialDNASequence(genex_initial_dna_sequence);
if (genex_saved_state === '') {
//Load DNA sequence from xml file
genex_dna_sequence = genex_initial_dna_sequence;
}
else {
//Load DNA sequence from saved value
genex_saved_state = JSON.parse(genex_saved_state);
genex_dna_sequence = genex_saved_state.genex_dna_sequence;
}
genexSetDNASequence(genex_dna_sequence);
//Now load mouse and keyboard handlers //Now load mouse and keyboard handlers
genexSetClickEvent(); genexSetClickEvent();
genexSetKeyEvent(); genexSetKeyEvent();
...@@ -35,10 +62,9 @@ ...@@ -35,10 +62,9 @@
var genex_problem_number = $('#genex_problem_number').val(); var genex_problem_number = $('#genex_problem_number').val();
genexSetProblemNumber(genex_problem_number); genexSetProblemNumber(genex_problem_number);
}; };
genexStoreAnswer = function(ans) { genexStoreAnswer = function(answer) {
var problem = $('#genex_container').parents('.problem'); var input_field = genexGetInputField();
var input_field = problem.find('input[type="hidden"][name!="dna_sequence"][name!="genex_problem_number"]'); var value = {'genex_dna_sequence': genexGetDNASequence(), 'genex_answer': answer};
input_field.val(ans); input_field.val(JSON.stringify(value));
}; };
}).call(this); }).call(this);
\ No newline at end of file
function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='3F4ADBED36D589545A9300A1EA686D36',Rb='73F4B6D6D466BAD6850A60128DF5B80D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Sb='BA18AC23ACC5016C5D0799E864BBDFFE',ub='Bad handler "',Tb='C7B18436BA03373FB13ED589C2CCF417',cc='DOMContentLoaded',Ub='E1A9A95677AFC620CAD5759B7ACC3E67',Vb='FF175D5583BDD5ACF40C7F0AFF9A374B',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b} function genex(){var P='',xb='" for "gwt:onLoadErrorFn"',vb='" for "gwt:onPropertyErrorFn"',ib='"><\/script>',Z='#',Xb='.cache.html',_='/',lb='//',Qb='46DBCB09BEC38A6DEE76494C6517111B',Rb='557C7018CDCA52B163256408948A1722',Sb='866AF633CAA7EA4DA7E906456CDEC65A',Tb='8F9C3F1A91187AA8391FD08BA7F8716D',Wb=':',pb='::',dc='<script defer="defer">genex.onInjectionDone(\'genex\')<\/script>',hb='<script id="',sb='=',$='?',Ub='A016796CF7FB22261AE1160531B5CF82',ub='Bad handler "',cc='DOMContentLoaded',Vb='F28D6C3D881F6C18E3357AAB004477EF',jb='SCRIPT',gb='__gwt_marker_genex',kb='base',cb='baseUrl',T='begin',S='bootstrap',bb='clear.cache.gif',rb='content',Y='end',Kb='gecko',Lb='gecko1_8',Q='genex',Yb='genex.css',eb='genex.nocache.js',ob='genex::',U='gwt.codesvr=',V='gwt.hosted=',W='gwt.hybrid',wb='gwt:onLoadErrorFn',tb='gwt:onPropertyErrorFn',qb='gwt:property',bc='head',Ob='hosted.html?genex',ac='href',Jb='ie6',Ib='ie8',Hb='ie9',yb='iframe',ab='img',zb="javascript:''",Zb='link',Nb='loadExternalRefs',mb='meta',Bb='moduleRequested',X='moduleStartup',Gb='msie',nb='name',Db='opera',Ab='position:absolute;width:0;height:0;border:none',$b='rel',Fb='safari',db='script',Pb='selectingPermutation',R='startup',_b='stylesheet',fb='undefined',Mb='unknown',Cb='user.agent',Eb='webkit';var m=window,n=document,o=m.__gwtStatsEvent?function(a){return m.__gwtStatsEvent(a)}:null,p=m.__gwtStatsSessionId?m.__gwtStatsSessionId:null,q,r,s,t=P,u={},v=[],w=[],x=[],y=0,z,A;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:T});if(!m.__gwt_stylesLoaded){m.__gwt_stylesLoaded={}}if(!m.__gwt_scriptsLoaded){m.__gwt_scriptsLoaded={}}function B(){var b=false;try{var c=m.location.search;return (c.indexOf(U)!=-1||(c.indexOf(V)!=-1||m.external&&m.external.gwtOnLoad))&&c.indexOf(W)==-1}catch(a){}B=function(){return b};return b}
function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}} function C(){if(q&&r){var b=n.getElementById(Q);var c=b.contentWindow;if(B()){c.__gwt_getProperty=function(a){return H(a)}}genex=null;c.gwtOnLoad(z,Q,t,y);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Y})}}
function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P} function D(){function e(a){var b=a.lastIndexOf(Z);if(b==-1){b=a.length}var c=a.indexOf($);if(c==-1){c=a.length}var d=a.lastIndexOf(_,Math.min(c,b));return d>=0?a.substring(0,d+1):P}
function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a} function f(a){if(a.match(/^\w+:\/\//)){}else{var b=n.createElement(ab);b.src=a+bb;a=e(b.src)}return a}
...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b} ...@@ -13,6 +13,6 @@ function F(a){var b=u[a];return b==null?null:b}
function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b} function G(a,b){var c=x;for(var d=0,e=a.length-1;d<e;++d){c=c[a[d]]||(c[a[d]]=[])}c[a[e]]=b}
function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null} function H(a){var b=w[a](),c=v[a];if(b in c){return b}var d=[];for(var e in c){d[c[e]]=e}if(A){A(a,d,b)}throw null}
var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}} var I;function J(){if(!I){I=true;var a=n.createElement(yb);a.src=zb;a.id=Q;a.style.cssText=Ab;a.tabIndex=-1;n.body.appendChild(a);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:X,millis:(new Date).getTime(),type:Bb});a.contentWindow.location.replace(t+L)}}
w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Hb],Qb);G([Fb],Rb);G([Ib],Sb);G([Lb],Tb);G([Db],Ub);G([Jb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}} w[Cb]=function(){var b=navigator.userAgent.toLowerCase();var c=function(a){return parseInt(a[1])*1000+parseInt(a[2])};if(function(){return b.indexOf(Db)!=-1}())return Db;if(function(){return b.indexOf(Eb)!=-1}())return Fb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=9}())return Hb;if(function(){return b.indexOf(Gb)!=-1&&n.documentMode>=8}())return Ib;if(function(){var a=/msie ([0-9]+)\.([0-9]+)/.exec(b);if(a&&a.length==3)return c(a)>=6000}())return Jb;if(function(){return b.indexOf(Kb)!=-1}())return Lb;return Mb};v[Cb]={gecko1_8:0,ie6:1,ie8:2,ie9:3,opera:4,safari:5};genex.onScriptLoad=function(){if(I){r=true;C()}};genex.onInjectionDone=function(){q=true;o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:Y});C()};E();D();var K;var L;if(B()){if(m.external&&(m.external.initModule&&m.external.initModule(Q))){m.location.reload();return}L=Ob;K=P}o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Pb});if(!B()){try{G([Ib],Qb);G([Fb],Rb);G([Db],Sb);G([Hb],Tb);G([Jb],Ub);G([Lb],Vb);K=x[H(Cb)];var M=K.indexOf(Wb);if(M!=-1){y=Number(K.substring(M+1));K=K.substring(0,M)}L=K+Xb}catch(a){return}}var N;function O(){if(!s){s=true;if(!__gwt_stylesLoaded[Yb]){var a=n.createElement(Zb);__gwt_stylesLoaded[Yb]=a;a.setAttribute($b,_b);a.setAttribute(ac,t+Yb);n.getElementsByTagName(bc)[0].appendChild(a)}C();if(n.removeEventListener){n.removeEventListener(cc,O,false)}if(N){clearInterval(N)}}}
if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)} if(n.addEventListener){n.addEventListener(cc,function(){J();O()},false)}var N=setInterval(function(){if(/loaded|complete/.test(n.readyState)){J();O()}},50);o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:S,millis:(new Date).getTime(),type:Y});o&&o({moduleName:Q,sessionId:p,subSystem:R,evtGroup:Nb,millis:(new Date).getTime(),type:T});n.write(dc)}
genex(); genex();
\ No newline at end of file
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