Merge branch 'master' of github.com:MITx/mitx into fix/cdodge/add-500-404-templates-to-Studio
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -13,14 +13,19 @@
|
||||
<script src="{% static 'js/vendor/jasmine-jquery.js' %}"></script>
|
||||
<script src="{% static 'console-runner.js' %}"></script>
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
{% for url in suite.static_files %}
|
||||
<script src="{{ STATIC_URL }}{{ url }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
{% compressed_js 'js-test-source' %}
|
||||
|
||||
{# source files #}
|
||||
{% for url in suite.js_files %}
|
||||
<script src="{{ url }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
{% load compressed %}
|
||||
{# static files #}
|
||||
{% compressed_js 'js-test-source' %}
|
||||
|
||||
{# spec files #}
|
||||
{% compressed_js 'spec' %}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', ->
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user