diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json
index 2249813b04..e7a66b5bc0 100644
--- a/cms/static/coffee/files.json
+++ b/cms/static/coffee/files.json
@@ -1,12 +1,12 @@
{
- "js_files": [
- "/static/js/vendor/RequireJS.js",
- "/static/js/vendor/jquery.min.js",
- "/static/js/vendor/jquery-ui.min.js",
- "/static/js/vendor/jquery.ui.draggable.js",
- "/static/js/vendor/jquery.cookie.js",
- "/static/js/vendor/json2.js",
- "/static/js/vendor/underscore-min.js",
- "/static/js/vendor/backbone-min.js"
+ "static_files": [
+ "js/vendor/RequireJS.js",
+ "js/vendor/jquery.min.js",
+ "js/vendor/jquery-ui.min.js",
+ "js/vendor/jquery.ui.draggable.js",
+ "js/vendor/jquery.cookie.js",
+ "js/vendor/json2.js",
+ "js/vendor/underscore-min.js",
+ "js/vendor/backbone-min.js"
]
}
diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py
index 391cac8eca..cad3110574 100644
--- a/cms/xmodule_namespace.py
+++ b/cms/xmodule_namespace.py
@@ -1,14 +1,27 @@
+"""
+Namespace defining common fields used by Studio for all blocks
+"""
+
import datetime
from xblock.core import Namespace, Boolean, Scope, ModelType, String
class StringyBoolean(Boolean):
+ """
+ Reads strings from JSON as booleans.
+
+ If the string is 'true' (case insensitive), then return True,
+ otherwise False.
+
+ JSON values that aren't strings are returned as is
+ """
def from_json(self, value):
if isinstance(value, basestring):
return value.lower() == 'true'
return value
+
class DateTuple(ModelType):
"""
ModelType that stores datetime objects as time tuples
@@ -24,6 +37,9 @@ class DateTuple(ModelType):
class CmsNamespace(Namespace):
+ """
+ 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_by = String(help="Id of the user who published this module", scope=Scope.settings)
diff --git a/common/lib/xmodule/xmodule/combined_open_ended_module.py b/common/lib/xmodule/xmodule/combined_open_ended_module.py
index f05f419a03..48fbfcced1 100644
--- a/common/lib/xmodule/xmodule/combined_open_ended_module.py
+++ b/common/lib/xmodule/xmodule/combined_open_ended_module.py
@@ -8,41 +8,66 @@ from xmodule.raw_module import RawDescriptor
from .x_module import XModule
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
+from collections import namedtuple
log = logging.getLogger("mitx.courseware")
-
V1_SETTINGS_ATTRIBUTES = ["display_name", "attempts", "is_graded", "accept_file_upload",
- "skip_spelling_checks", "due", "graceperiod", "max_score"]
+ "skip_spelling_checks", "due", "graceperiod", "max_score"]
V1_STUDENT_ATTRIBUTES = ["current_task_number", "task_states", "state",
- "student_attempts", "ready_to_reset"]
+ "student_attempts", "ready_to_reset"]
V1_ATTRIBUTES = V1_SETTINGS_ATTRIBUTES + V1_STUDENT_ATTRIBUTES
-VERSION_TUPLES = (
- ('1', CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES, V1_STUDENT_ATTRIBUTES),
-)
+VersionTuple = namedtuple('VersionTuple', ['descriptor', 'module', 'settings_attributes', 'student_attributes'])
+VERSION_TUPLES = {
+ 1: VersionTuple(CombinedOpenEndedV1Descriptor, CombinedOpenEndedV1Module, V1_SETTINGS_ATTRIBUTES,
+ V1_STUDENT_ATTRIBUTES),
+}
DEFAULT_VERSION = 1
-DEFAULT_VERSION = str(DEFAULT_VERSION)
+
+
+class VersionInteger(Integer):
+ """
+ A model type that converts from strings to integers when reading from json.
+ Also does error checking to see if version is correct or not.
+ """
+
+ def from_json(self, value):
+ try:
+ value = int(value)
+ if value not in VERSION_TUPLES:
+ version_error_string = "Could not find version {0}, using version {1} instead"
+ log.error(version_error_string.format(value, DEFAULT_VERSION))
+ value = DEFAULT_VERSION
+ except:
+ value = DEFAULT_VERSION
+ return value
class CombinedOpenEndedFields(object):
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)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.student_state)
- state = String(help="Which step within the current task that the student is on.", default="initial", scope=Scope.student_state)
- student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
- ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False, scope=Scope.student_state)
+ state = String(help="Which step within the current task that the student is on.", default="initial",
+ scope=Scope.student_state)
+ student_attempts = Integer(help="Number of attempts taken by the student on this problem", default=0,
+ scope=Scope.student_state)
+ ready_to_reset = Boolean(help="If the problem is ready to be reset or not.", default=False,
+ scope=Scope.student_state)
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)
- accept_file_upload = Boolean(help="Whether or not the problem accepts file uploads.", default=False, scope=Scope.settings)
- skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True, 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,
+ scope=Scope.settings)
+ skip_spelling_checks = Boolean(help="Whether or not to skip initial spelling checks.", default=True,
+ scope=Scope.settings)
due = String(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, scope=Scope.settings)
+ graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
+ scope=Scope.settings)
max_score = Integer(help="Maximum score for the problem.", default=1, scope=Scope.settings)
- version = Integer(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)
@@ -130,23 +155,10 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
if self.task_states is None:
self.task_states = []
- versions = [i[0] for i in VERSION_TUPLES]
- descriptors = [i[1] for i in VERSION_TUPLES]
- modules = [i[2] for i in VERSION_TUPLES]
- settings_attributes = [i[3] for i in VERSION_TUPLES]
- student_attributes = [i[4] for i in VERSION_TUPLES]
- version_error_string = "Could not find version {0}, using version {1} instead"
+ version_tuple = VERSION_TUPLES[self.version]
- try:
- version_index = versions.index(self.version)
- except:
- #This is a dev_facing_error
- log.error(version_error_string.format(self.version, DEFAULT_VERSION))
- self.version = DEFAULT_VERSION
- version_index = versions.index(self.version)
-
- self.student_attributes = student_attributes[version_index]
- self.settings_attributes = settings_attributes[version_index]
+ self.student_attributes = version_tuple.student_attributes
+ self.settings_attributes = version_tuple.settings_attributes
attributes = self.student_attributes + self.settings_attributes
@@ -154,10 +166,11 @@ class CombinedOpenEndedModule(CombinedOpenEndedFields, XModule):
'rewrite_content_links': self.rewrite_content_links,
}
instance_state = {k: getattr(self, k) for k in attributes}
- self.child_descriptor = descriptors[version_index](self.system)
- self.child_definition = descriptors[version_index].definition_from_xml(etree.fromstring(self.data), self.system)
- self.child_module = modules[version_index](self.system, location, self.child_definition, self.child_descriptor,
- instance_state=instance_state, static_data=static_data, attributes=attributes)
+ self.child_descriptor = version_tuple.descriptor(self.system)
+ self.child_definition = version_tuple.descriptor.definition_from_xml(etree.fromstring(self.data), self.system)
+ self.child_module = version_tuple.module(self.system, location, self.child_definition, self.child_descriptor,
+ instance_state=instance_state, static_data=static_data,
+ attributes=attributes)
self.save_instance_data()
def get_html(self):
diff --git a/common/templates/jasmine/base.html b/common/templates/jasmine/base.html
index 96507bdebf..9a1b3bed92 100644
--- a/common/templates/jasmine/base.html
+++ b/common/templates/jasmine/base.html
@@ -13,14 +13,19 @@
+ {% load compressed %}
+ {# static files #}
+ {% for url in suite.static_files %}
+
+ {% endfor %}
+
+ {% compressed_js 'js-test-source' %}
+
{# source files #}
{% for url in suite.js_files %}
{% endfor %}
- {% load compressed %}
- {# static files #}
- {% compressed_js 'js-test-source' %}
{# spec files #}
{% compressed_js 'spec' %}
diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py
index 0be5724365..671283db9f 100644
--- a/lms/djangoapps/instructor/views.py
+++ b/lms/djangoapps/instructor/views.py
@@ -92,9 +92,15 @@ def instructor_dashboard(request, course_id):
data += compute_course_stats(course).items()
if request.user.is_staff:
for field in course.fields:
+ if getattr(field.scope, 'student', False):
+ continue
+
data.append([field.name, json.dumps(field.read_json(course))])
for namespace in course.namespaces:
for field in getattr(course, namespace).fields:
+ if getattr(field.scope, 'student', False):
+ continue
+
data.append(["{}.{}".format(namespace, field.name), json.dumps(field.read_json(course))])
datatable['data'] = data
diff --git a/lms/static/coffee/files.json b/lms/static/coffee/files.json
index 5dc03613b9..0efe488dd9 100644
--- a/lms/static/coffee/files.json
+++ b/lms/static/coffee/files.json
@@ -5,8 +5,5 @@
"/static/js/vendor/jquery-ui.min.js",
"/static/js/vendor/jquery.leanModal.min.js",
"/static/js/vendor/flot/jquery.flot.js"
- ],
- "static_files": [
- "js/application.js"
]
}
diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee
index 072d220a44..8258d8965a 100644
--- a/lms/static/coffee/spec/calculator_spec.coffee
+++ b/lms/static/coffee/spec/calculator_spec.coffee
@@ -4,9 +4,6 @@ describe 'Calculator', ->
@calculator = new Calculator
describe 'bind', ->
- beforeEach ->
- Calculator.bind()
-
it 'bind the calculator button', ->
expect($('.calc')).toHandleWith 'click', @calculator.toggle
@@ -31,12 +28,19 @@ describe 'Calculator', ->
$('form#calculator').submit()
describe 'toggle', ->
- it 'toggle the calculator and focus the input', ->
- spyOn $.fn, 'focus'
- @calculator.toggle(jQuery.Event("click"))
+ it 'focuses the input when toggled', ->
- expect($('li.calc-main')).toHaveClass('open')
- expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
+ # Since the focus is called asynchronously, we need to
+ # wait until focus() is called.
+ didFocus = false
+ runs ->
+ spyOn($.fn, 'focus').andCallFake (elementName) -> didFocus = true
+ @calculator.toggle(jQuery.Event("click"))
+
+ waitsFor (-> didFocus), "focus() should have been called on the input", 1000
+
+ runs ->
+ expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
it 'toggle the close button on the calculator button', ->
@calculator.toggle(jQuery.Event("click"))
diff --git a/lms/static/coffee/spec/modules/tab_spec.coffee b/lms/static/coffee/spec/modules/tab_spec.coffee
index 909f0d7cda..6fba470974 100644
--- a/lms/static/coffee/spec/modules/tab_spec.coffee
+++ b/lms/static/coffee/spec/modules/tab_spec.coffee
@@ -22,18 +22,23 @@ describe 'Tab', ->
it 'bind the tabs', ->
expect($.fn.tabs).toHaveBeenCalledWith show: @tab.onShow
+ # As of jQuery 1.9, the onShow callback is deprecated
+ # http://jqueryui.com/upgrade-guide/1.9/#deprecated-show-event-renamed-to-activate
+ # The code below tests that onShow does what is expected,
+ # but note that onShow will NOT be called when the user
+ # clicks on the tab if we're using jQuery version >= 1.9
describe 'onShow', ->
beforeEach ->
@tab = new Tab 1, @items
- $('[href="#tab-1-0"]').click()
+ @tab.onShow($('#tab-1-0'), {'index': 1})
it 'replace content in the container', ->
- $('[href="#tab-1-1"]').click()
+ @tab.onShow($('#tab-1-1'), {'index': 1})
expect($('#tab-1-0').html()).toEqual ''
expect($('#tab-1-1').html()).toEqual 'Video 2'
expect($('#tab-1-2').html()).toEqual ''
it 'trigger contentChanged event on the element', ->
spyOnEvent @tab.el, 'contentChanged'
- $('[href="#tab-1-1"]').click()
+ @tab.onShow($('#tab-1-1'), {'index': 1})
expect('contentChanged').toHaveBeenTriggeredOn @tab.el
diff --git a/lms/static/coffee/spec/navigation_spec.coffee b/lms/static/coffee/spec/navigation_spec.coffee
index 1340984e52..b351164b63 100644
--- a/lms/static/coffee/spec/navigation_spec.coffee
+++ b/lms/static/coffee/spec/navigation_spec.coffee
@@ -32,11 +32,9 @@ describe 'Navigation', ->
heightStyle: 'content'
it 'binds the accordionchange event', ->
- Navigation.bind()
expect($('#accordion')).toHandleWith 'accordionchange', @navigation.log
it 'bind the navigation toggle', ->
- Navigation.bind()
expect($('#open_close_accordion a')).toHandleWith 'click', @navigation.toggle
describe 'when the #accordion does not exists', ->
@@ -45,7 +43,6 @@ describe 'Navigation', ->
it 'does not activate the accordion', ->
spyOn $.fn, 'accordion'
- Navigation.bind()
expect($('#accordion').accordion).wasNotCalled()
describe 'toggle', ->
diff --git a/lms/xmodule_namespace.py b/lms/xmodule_namespace.py
index 4c04700a31..423c0eb0ec 100644
--- a/lms/xmodule_namespace.py
+++ b/lms/xmodule_namespace.py
@@ -1,14 +1,28 @@
-from xblock.core import Namespace, Boolean, Scope, String, List, Float
+"""
+Namespace that defines fields common to all blocks used in the LMS
+"""
+from xblock.core import Namespace, Boolean, Scope, String, Float
from xmodule.fields import Date, Timedelta
class StringyBoolean(Boolean):
+ """
+ Reads strings from JSON as booleans.
+
+ 'true' (case insensitive) return True, other strings return False
+ Other types are returned unchanged
+ """
def from_json(self, value):
if isinstance(value, basestring):
return value.lower() == 'true'
return value
+
class StringyFloat(Float):
+ """
+ Reads values as floats. If the value parses as a float, returns
+ that, otherwise returns None
+ """
def from_json(self, value):
try:
return float(value)
@@ -17,6 +31,9 @@ class StringyFloat(Float):
class LmsNamespace(Namespace):
+ """
+ Namespace that defines fields common to all blocks used in the LMS
+ """
hide_from_toc = StringyBoolean(
help="Whether to display this module in the table of contents",
default=False,
@@ -38,8 +55,14 @@ class LmsNamespace(Namespace):
source_file = String(help="DO NOT USE", scope=Scope.settings)
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
- graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
+ graceperiod = Timedelta(
+ help="Amount of time after the due date that submissions will be accepted",
+ scope=Scope.settings
+ )
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
rerandomize = String(help="When to rerandomize the problem", default="always", scope=Scope.settings)
- days_early_for_beta = StringyFloat(help="Number of days early to show content to beta users", default=None, scope=Scope.settings)
-
+ days_early_for_beta = StringyFloat(
+ help="Number of days early to show content to beta users",
+ default=None,
+ scope=Scope.settings
+ )