Commit 0014b289 by Victor Shnayder

Merge remote-tracking branch 'origin/master' into 700x-sandbox

parents c81abe1e 75449359
import logging
from static_replace import replace_static_urls from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from django.http import Http404
from lxml import etree
import re
from django.http import HttpResponseBadRequest, Http404
def get_module_info(store, location, parent_location=None, rewrite_static_links=False): def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
try: try:
if location.revision is None: if location.revision is None:
module = store.get_item(location) module = store.get_item(location)
else: else:
module = store.get_item(location) module = store.get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
raise Http404 # create a new one
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
data = module.definition['data'] data = module.definition['data']
if rewrite_static_links: if rewrite_static_links:
data = replace_static_urls( data = replace_static_urls(
module.definition['data'], module.definition['data'],
None,
course_namespace=Location([
module.location.tag,
module.location.org,
module.location.course,
None, None,
None course_namespace=Location([
]) module.location.tag,
) module.location.org,
module.location.course,
None,
None
])
)
return { return {
'id': module.location.url(), 'id': module.location.url(),
'data': data, 'data': data,
'metadata': module.metadata 'metadata': module.metadata
...@@ -39,58 +37,56 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links= ...@@ -39,58 +37,56 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
def set_module_info(store, location, post_data): def set_module_info(store, location, post_data):
module = None module = None
isNew = False try:
try: if location.revision is None:
if location.revision is None: module = store.get_item(location)
module = store.get_item(location) else:
else: module = store.get_item(location)
module = store.get_item(location) except:
except: pass
pass
if module is None: if module is None:
# new module at this location # new module at this location
# presume that we have an 'Empty' template # presume that we have an 'Empty' template
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty']) template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location) module = store.clone_item(template_location, location)
isNew = True
if post_data.get('data') is not None: if post_data.get('data') is not None:
data = post_data['data'] data = post_data['data']
store.update_item(location, data) store.update_item(location, data)
# cdodge: note calling request.POST.get('children') will return None if children is an empty array # cdodge: note calling request.POST.get('children') will return None if children is an empty array
# so it lead to a bug whereby the last component to be deleted in the UI was not actually # so it lead to a bug whereby the last component to be deleted in the UI was not actually
# deleting the children object from the children collection # deleting the children object from the children collection
if 'children' in post_data and post_data['children'] is not None: if 'children' in post_data and post_data['children'] is not None:
children = post_data['children'] children = post_data['children']
store.update_children(location, children) store.update_children(location, children)
# cdodge: also commit any metadata which might have been passed along in the # cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there # POST from the client, if it is there
# NOTE, that the postback is not the complete metadata, as there's system metadata which is # NOTE, that the postback is not the complete metadata, as there's system metadata which is
# not presented to the end-user for editing. So let's fetch the original and # not presented to the end-user for editing. So let's fetch the original and
# 'apply' the submitted metadata, so we don't end up deleting system metadata # 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None: if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata'] posted_metadata = post_data['metadata']
# update existing metadata with submitted metadata (which can be partial) # update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it' # IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys(): for metadata_key in posted_metadata.keys():
# let's strip out any metadata fields from the postback which have been identified as system metadata # let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client # and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in module.system_metadata_fields: if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None: elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata: if metadata_key in module.metadata:
del module.metadata[metadata_key] del module.metadata[metadata_key]
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates # overlay the new metadata over the modulestore sourced collection to support partial updates
module.metadata.update(posted_metadata) module.metadata.update(posted_metadata)
# commit to datastore # commit to datastore
store.update_metadata(location, module.metadata) store.update_metadata(location, module.metadata)
...@@ -122,7 +122,8 @@ def index(request): ...@@ -122,7 +122,8 @@ def index(request):
course.location.course, course.location.course,
course.location.name])) course.location.name]))
for course in courses], for course in courses],
'user': request.user 'user': request.user,
'disable_course_creation': settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff
}) })
...@@ -1259,6 +1260,10 @@ def edge(request): ...@@ -1259,6 +1260,10 @@ def edge(request):
@login_required @login_required
@expect_json @expect_json
def create_new_course(request): def create_new_course(request):
if settings.MITX_FEATURES.get('DISABLE_COURSE_CREATION', False) and not request.user.is_staff:
raise PermissionDenied()
# This logic is repeated in xmodule/modulestore/tests/factories.py # This logic is repeated in xmodule/modulestore/tests/factories.py
# so if you change anything here, you need to also change it there. # so if you change anything here, you need to also change it there.
# TODO: write a test that creates two courses, one with the factory and # TODO: write a test that creates two courses, one with the factory and
......
...@@ -165,13 +165,6 @@ STATICFILES_DIRS = [ ...@@ -165,13 +165,6 @@ STATICFILES_DIRS = [
# This is how you would use the textbook images locally # This is how you would use the textbook images locally
# ("book", ENV_ROOT / "book_images") # ("book", ENV_ROOT / "book_images")
] ]
if os.path.isdir(GITHUB_REPO_ROOT):
STATICFILES_DIRS += [
# TODO (cpennington): When courses aren't loaded from github, remove this
(course_dir, GITHUB_REPO_ROOT / course_dir)
for course_dir in os.listdir(GITHUB_REPO_ROOT)
if os.path.isdir(GITHUB_REPO_ROOT / course_dir)
]
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
......
...@@ -37,7 +37,9 @@ ...@@ -37,7 +37,9 @@
<h1>My Courses</h1> <h1>My Courses</h1>
<article class="my-classes"> <article class="my-classes">
% if user.is_active: % if user.is_active:
<a href="#" class="new-button new-course-button"><span class="plus-icon white"></span> New Course</a> % if not disable_course_creation:
<a href="#" class="new-button new-course-button"><span class="plus-icon white"></span> New Course</a>
%endif
<ul class="class-list"> <ul class="class-list">
%for course, url in courses: %for course, url in courses:
<li> <li>
......
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
$cancelButton.bind('click', hideNewUserForm); $cancelButton.bind('click', hideNewUserForm);
$('.new-user-button').bind('click', showNewUserForm); $('.new-user-button').bind('click', showNewUserForm);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel); $('body').bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
$('.remove-user').click(function() { $('.remove-user').click(function() {
$.ajax({ $.ajax({
......
...@@ -206,7 +206,7 @@ from contentstore import utils ...@@ -206,7 +206,7 @@ from contentstore import utils
<section class="setting-details-marketing"> <section class="setting-details-marketing">
<header> <header>
<h3>Introducing Your Course</h3> <h3>Introducing Your Course</h3>
<span class="detail">Information for perspective students</span> <span class="detail">Information for prospective students</span>
</header> </header>
<div class="row row-col2"> <div class="row row-col2">
......
...@@ -999,7 +999,7 @@ def sympy_check2(): ...@@ -999,7 +999,7 @@ def sympy_check2():
self.context['debug'] = self.system.DEBUG self.context['debug'] = self.system.DEBUG
# exec the check function # exec the check function
if type(self.code) == str: if isinstance(self.code, basestring):
try: try:
exec self.code in self.context['global_context'], self.context exec self.code in self.context['global_context'], self.context
correct = self.context['correct'] correct = self.context['correct']
......
test_problem_display.js
test_problem_generator.js
test_problem_grader.js
xproblem.js
\ No newline at end of file
// Generated by CoffeeScript 1.4.0
(function() {
var MinimaxProblemDisplay, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
MinimaxProblemDisplay = (function(_super) {
__extends(MinimaxProblemDisplay, _super);
function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
this.state = state;
this.submission = submission;
this.evaluation = evaluation;
this.container = container;
this.submissionField = submissionField;
this.parameters = parameters != null ? parameters : {};
MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters);
}
MinimaxProblemDisplay.prototype.render = function() {};
MinimaxProblemDisplay.prototype.createSubmission = function() {
var id, value, _ref, _results;
this.newSubmission = {};
if (this.submission != null) {
_ref = this.submission;
_results = [];
for (id in _ref) {
value = _ref[id];
_results.push(this.newSubmission[id] = value);
}
return _results;
}
};
MinimaxProblemDisplay.prototype.getCurrentSubmission = function() {
return this.newSubmission;
};
return MinimaxProblemDisplay;
})(XProblemDisplay);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.TestProblemDisplay = TestProblemDisplay;
}).call(this);
// Generated by CoffeeScript 1.4.0
(function() {
var TestProblemGenerator, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
TestProblemGenerator = (function(_super) {
__extends(TestProblemGenerator, _super);
function TestProblemGenerator(seed, parameters) {
this.parameters = parameters != null ? parameters : {};
TestProblemGenerator.__super__.constructor.call(this, seed, this.parameters);
}
TestProblemGenerator.prototype.generate = function() {
this.problemState.value = this.parameters.value;
return this.problemState;
};
return TestProblemGenerator;
})(XProblemGenerator);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.generatorClass = TestProblemGenerator;
}).call(this);
// Generated by CoffeeScript 1.4.0
(function() {
var TestProblemGrader, root,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
TestProblemGrader = (function(_super) {
__extends(TestProblemGrader, _super);
function TestProblemGrader(submission, problemState, parameters) {
this.submission = submission;
this.problemState = problemState;
this.parameters = parameters != null ? parameters : {};
TestProblemGrader.__super__.constructor.call(this, this.submission, this.problemState, this.parameters);
}
TestProblemGrader.prototype.solve = function() {
return this.solution = {
0: this.problemState.value
};
};
TestProblemGrader.prototype.grade = function() {
var allCorrect, id, value, valueCorrect, _ref;
if (!(this.solution != null)) {
this.solve();
}
allCorrect = true;
_ref = this.solution;
for (id in _ref) {
value = _ref[id];
valueCorrect = this.submission != null ? value === this.submission[id] : false;
this.evaluation[id] = valueCorrect;
if (!valueCorrect) {
allCorrect = false;
}
}
return allCorrect;
};
return TestProblemGrader;
})(XProblemGrader);
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.graderClass = TestProblemGrader;
}).call(this);
// Generated by CoffeeScript 1.4.0
(function() {
var XProblemDisplay, XProblemGenerator, XProblemGrader, root;
XProblemGenerator = (function() {
function XProblemGenerator(seed, parameters) {
this.parameters = parameters != null ? parameters : {};
this.random = new MersenneTwister(seed);
this.problemState = {};
}
XProblemGenerator.prototype.generate = function() {
return console.error("Abstract method called: XProblemGenerator.generate");
};
return XProblemGenerator;
})();
XProblemDisplay = (function() {
function XProblemDisplay(state, submission, evaluation, container, submissionField, parameters) {
this.state = state;
this.submission = submission;
this.evaluation = evaluation;
this.container = container;
this.submissionField = submissionField;
this.parameters = parameters != null ? parameters : {};
}
XProblemDisplay.prototype.render = function() {
return console.error("Abstract method called: XProblemDisplay.render");
};
XProblemDisplay.prototype.updateSubmission = function() {
return this.submissionField.val(JSON.stringify(this.getCurrentSubmission()));
};
XProblemDisplay.prototype.getCurrentSubmission = function() {
return console.error("Abstract method called: XProblemDisplay.getCurrentSubmission");
};
return XProblemDisplay;
})();
XProblemGrader = (function() {
function XProblemGrader(submission, problemState, parameters) {
this.submission = submission;
this.problemState = problemState;
this.parameters = parameters != null ? parameters : {};
this.solution = null;
this.evaluation = {};
}
XProblemGrader.prototype.solve = function() {
return console.error("Abstract method called: XProblemGrader.solve");
};
XProblemGrader.prototype.grade = function() {
return console.error("Abstract method called: XProblemGrader.grade");
};
return XProblemGrader;
})();
root = typeof exports !== "undefined" && exports !== null ? exports : this;
root.XProblemGenerator = XProblemGenerator;
root.XProblemDisplay = XProblemDisplay;
root.XProblemGrader = XProblemGrader;
}).call(this);
...@@ -34,6 +34,7 @@ setup( ...@@ -34,6 +34,7 @@ setup(
"section = xmodule.backcompat_module:SemanticSectionDescriptor", "section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor", "video = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
......
...@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor):
raise ValueError("First appointment date must be before last appointment date") raise ValueError("First appointment date must be before last appointment date")
if self.registration_end_date > self.last_eligible_appointment_date: if self.registration_end_date > self.last_eligible_appointment_date:
raise ValueError("Registration end date must be before last appointment date") raise ValueError("Registration end date must be before last appointment date")
self.exam_url = exam_info.get('Exam_URL')
def _try_parse_time(self, key): def _try_parse_time(self, key):
""" """
...@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor):
else: else:
return None return None
def get_test_center_exam(self, exam_series_code):
exams = [exam for exam in self.test_center_exams if exam.exam_series_code == exam_series_code]
return exams[0] if len(exams) == 1 else None
@property @property
def title(self): def title(self):
return self.display_name return self.display_name
......
...@@ -107,12 +107,13 @@ class @HTMLEditingDescriptor ...@@ -107,12 +107,13 @@ class @HTMLEditingDescriptor
# In order for isDirty() to return true ONLY if edits have been made after setting the text, # In order for isDirty() to return true ONLY if edits have been made after setting the text,
# both the startContent must be sync'ed up and the dirty flag set to false. # both the startContent must be sync'ed up and the dirty flag set to false.
visualEditor.startContent = visualEditor.getContent({format: "raw", no_events: 1}); visualEditor.startContent = visualEditor.getContent({format: "raw", no_events: 1});
visualEditor.isNotDirty = true
@focusVisualEditor(visualEditor) @focusVisualEditor(visualEditor)
@showingVisualEditor = true @showingVisualEditor = true
focusVisualEditor: (visualEditor) => focusVisualEditor: (visualEditor) =>
visualEditor.focus() visualEditor.focus()
# Need to mark editor as not dirty both when it is initially created and when we switch back to it.
visualEditor.isNotDirty = true
if not @$mceToolbar? if not @$mceToolbar?
@$mceToolbar = $(@element).find('table.mceToolbar') @$mceToolbar = $(@element).find('table.mceToolbar')
......
...@@ -73,7 +73,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -73,7 +73,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check) # VS[compat]. Take this out once course conversion is done (perhaps leave the uniqueness check)
# tags that really need unique names--they store (or should store) state. # tags that really need unique names--they store (or should store) state.
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence') need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'timelimit')
attr = xml_data.attrib attr = xml_data.attrib
tag = xml_data.tag tag = xml_data.tag
......
...@@ -84,18 +84,21 @@ class ConditionalModuleTest(unittest.TestCase): ...@@ -84,18 +84,21 @@ class ConditionalModuleTest(unittest.TestCase):
descriptor = self.modulestore.get_instance(course.id, location, depth=None) descriptor = self.modulestore.get_instance(course.id, location, depth=None)
location = descriptor.location location = descriptor.location
instance_state = instance_states.get(location.category, None) instance_state = instance_states.get(location.category, None)
print "inner_get_module, location.category=%s, inst_state=%s" % (location.category, instance_state) print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state)
return descriptor.xmodule_constructor(test_system)(instance_state, shared_state) return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"]) location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
module = inner_get_module(location)
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None): def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text return text
test_system.replace_urls = replace_urls test_system.replace_urls = replace_urls
test_system.get_module = inner_get_module test_system.get_module = inner_get_module
module = inner_get_module(location)
print "module: ", module print "module: ", module
print "module definition: ", module.definition
print "module children: ", module.get_children()
print "module display items (children): ", module.get_display_items()
html = module.get_html() html = module.get_html()
print "html type: ", type(html) print "html type: ", type(html)
......
import json
import logging
from lxml import etree
from time import time
from xmodule.editing_module import XMLEditingDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError
log = logging.getLogger(__name__)
class TimeLimitModule(XModule):
'''
Wrapper module which imposes a time constraint for the completion of its child.
'''
def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
self.rendered = False
self.beginning_at = None
self.ending_at = None
self.accommodation_code = None
if instance_state is not None:
state = json.loads(instance_state)
if 'beginning_at' in state:
self.beginning_at = state['beginning_at']
if 'ending_at' in state:
self.ending_at = state['ending_at']
if 'accommodation_code' in state:
self.accommodation_code = state['accommodation_code']
# For a timed activity, we are only interested here
# in time-related accommodations, and these should be disjoint.
# (For proctored exams, it is possible to have multiple accommodations
# apply to an exam, so they require accommodating a multi-choice.)
TIME_ACCOMMODATION_CODES = (('NONE', 'No Time Accommodation'),
('ADDHALFTIME', 'Extra Time - 1 1/2 Time'),
('ADD30MIN', 'Extra Time - 30 Minutes'),
('DOUBLE', 'Extra Time - Double Time'),
('TESTING', 'Extra Time -- Large amount for testing purposes')
)
def _get_accommodated_duration(self, duration):
'''
Get duration for activity, as adjusted for accommodations.
Input and output are expressed in seconds.
'''
if self.accommodation_code is None or self.accommodation_code == 'NONE':
return duration
elif self.accommodation_code == 'ADDHALFTIME':
# TODO: determine what type to return
return int(duration * 1.5)
elif self.accommodation_code == 'ADD30MIN':
return (duration + (30 * 60))
elif self.accommodation_code == 'DOUBLE':
return (duration * 2)
elif self.accommodation_code == 'TESTING':
# when testing, set timer to run for a week at a time.
return 3600 * 24 * 7
@property
def has_begun(self):
return self.beginning_at is not None
@property
def has_ended(self):
if not self.ending_at:
return False
return self.ending_at < time()
def begin(self, duration):
'''
Sets the starting time and ending time for the activity,
based on the duration provided (in seconds).
'''
self.beginning_at = time()
modified_duration = self._get_accommodated_duration(duration)
self.ending_at = self.beginning_at + modified_duration
def get_end_time_in_ms(self):
return int(self.ending_at * 1000)
def get_instance_state(self):
state = {}
if self.beginning_at:
state['beginning_at'] = self.beginning_at
if self.ending_at:
state['ending_at'] = self.ending_at
if self.accommodation_code:
state['accommodation_code'] = self.accommodation_code
return json.dumps(state)
def get_html(self):
self.render()
return self.content
def get_progress(self):
''' Return the total progress, adding total done and total available.
(assumes that each submodule uses the same "units" for progress.)
'''
# TODO: Cache progress or children array?
children = self.get_children()
progresses = [child.get_progress() for child in children]
progress = reduce(Progress.add_counts, progresses)
return progress
def handle_ajax(self, dispatch, get):
raise NotFoundError('Unexpected dispatch type')
def render(self):
if self.rendered:
return
# assumes there is one and only one child, so it only renders the first child
children = self.get_display_items()
if children:
child = children[0]
self.content = child.get_html()
self.rendered = True
def get_icon_class(self):
children = self.get_children()
if children:
return children[0].get_icon_class()
else:
return "other"
class TimeLimitDescriptor(XMLEditingDescriptor, XmlDescriptor):
module_class = TimeLimitModule
# For remembering when a student started, and when they should end
stores_state = True
@classmethod
def definition_from_xml(cls, xml_object, system):
children = []
for child in xml_object:
try:
children.append(system.process_xml(etree.tostring(child, encoding='unicode')).location.url())
except Exception as e:
log.exception("Unable to load child when parsing TimeLimit wrapper. Continuing...")
if system.error_tracker is not None:
system.error_tracker("ERROR: " + str(e))
continue
return {'children': children}
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('timelimit')
for child in self.get_children():
xml_object.append(
etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object
...@@ -13,14 +13,8 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types ...@@ -13,14 +13,8 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types
""" """
from django.db import models from django.db import models
#from django.core.cache import cache
from django.contrib.auth.models import User from django.contrib.auth.models import User
#from cache_toolbox import cache_model, cache_relation
#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
class StudentModule(models.Model): class StudentModule(models.Model):
""" """
Keeps student state for a particular module in a particular course. Keeps student state for a particular module in a particular course.
...@@ -30,6 +24,7 @@ class StudentModule(models.Model): ...@@ -30,6 +24,7 @@ class StudentModule(models.Model):
MODULE_TYPES = (('problem', 'problem'), MODULE_TYPES = (('problem', 'problem'),
('video', 'video'), ('video', 'video'),
('html', 'html'), ('html', 'html'),
('timelimit', 'timelimit'),
) )
## These three are the key for the object ## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True) module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
......
...@@ -8,7 +8,7 @@ from django.core.context_processors import csrf ...@@ -8,7 +8,7 @@ from django.core.context_processors import csrf
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import Http404 from django.http import Http404, HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string from mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie #from django.views.decorators.csrf import ensure_csrf_cookie
...@@ -20,7 +20,7 @@ from courseware.access import has_access ...@@ -20,7 +20,7 @@ from courseware.access import has_access
from courseware.courses import (get_courses, get_course_with_access, from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement) get_courses_by_university, sort_by_announcement)
import courseware.tabs as tabs import courseware.tabs as tabs
from courseware.models import StudentModuleCache from courseware.models import StudentModule, StudentModuleCache
from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor from module_render import toc_for_course, get_module, get_instance_module, get_module_for_descriptor
from django_comment_client.utils import get_discussion_title from django_comment_client.utils import get_discussion_title
...@@ -153,6 +153,76 @@ def save_child_position(seq_module, child_name, instance_module): ...@@ -153,6 +153,76 @@ def save_child_position(seq_module, child_name, instance_module):
instance_module.state = seq_module.get_instance_state() instance_module.state = seq_module.get_instance_state()
instance_module.save() instance_module.save()
def check_for_active_timelimit_module(request, course_id, course):
'''
Looks for a timing module for the given user and course that is currently active.
If found, returns a context dict with timer-related values to enable display of time remaining.
'''
context = {}
timelimit_student_modules = StudentModule.objects.filter(student=request.user, course_id=course_id, module_type='timelimit')
if timelimit_student_modules:
for timelimit_student_module in timelimit_student_modules:
# get the corresponding section_descriptor for the given StudentModel entry:
module_state_key = timelimit_student_module.module_state_key
timelimit_descriptor = modulestore().get_instance(course_id, Location(module_state_key))
timelimit_module_cache = StudentModuleCache.cache_for_descriptor_descendents(course.id, request.user,
timelimit_descriptor, depth=None)
timelimit_module = get_module_for_descriptor(request.user, request, timelimit_descriptor,
timelimit_module_cache, course.id, position=None)
if timelimit_module is not None and timelimit_module.category == 'timelimit' and \
timelimit_module.has_begun and not timelimit_module.has_ended:
location = timelimit_module.location
# determine where to go when the timer expires:
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', location))
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
context['time_expired_redirect_url'] = time_expired_redirect_url
# Fetch the end time (in GMT) as stored in the module when it was started.
# This value should be UTC time as number of milliseconds since epoch.
end_date = timelimit_module.get_end_time_in_ms()
context['timer_expiration_datetime'] = end_date
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
return_url = reverse('jump_to', kwargs={'course_id':course_id, 'location':location})
context['timer_navigation_return_url'] = return_url
return context
def update_timelimit_module(user, course_id, student_module_cache, timelimit_descriptor, timelimit_module):
'''
Updates the state of the provided timing module, starting it if it hasn't begun.
Returns dict with timer-related values to enable display of time remaining.
Returns 'timer_expiration_datetime' in dict if timer is still active, and not if timer has expired.
'''
context = {}
# determine where to go when the exam ends:
if 'time_expired_redirect_url' not in timelimit_descriptor.metadata:
raise Http404("No {0} metadata at this location: {1} ".format('time_expired_redirect_url', timelimit_module.location))
time_expired_redirect_url = timelimit_descriptor.metadata.get('time_expired_redirect_url')
context['time_expired_redirect_url'] = time_expired_redirect_url
if not timelimit_module.has_ended:
if not timelimit_module.has_begun:
# user has not started the exam, so start it now.
if 'duration' not in timelimit_descriptor.metadata:
raise Http404("No {0} metadata at this location: {1} ".format('duration', timelimit_module.location))
# The user may have an accommodation that has been granted to them.
# This accommodation information should already be stored in the module's state.
duration = int(timelimit_descriptor.metadata.get('duration'))
timelimit_module.begin(duration)
# we have changed state, so we need to persist the change:
instance_module = get_instance_module(course_id, user, timelimit_module, student_module_cache)
instance_module.state = timelimit_module.get_instance_state()
instance_module.save()
# the exam has been started, either because the student is returning to the
# exam page, or because they have just visited it. Fetch the end time (in GMT) as stored
# in the module when it was started.
# This value should be UTC time as number of milliseconds since epoch.
context['timer_expiration_datetime'] = timelimit_module.get_end_time_in_ms()
# also use the timed module to determine whether top-level navigation is visible:
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
return context
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -222,7 +292,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -222,7 +292,7 @@ def index(request, course_id, chapter=None, section=None,
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache) instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
save_child_position(course_module, chapter, instance_module) save_child_position(course_module, chapter, instance_module)
else: else:
raise Http404 raise Http404('No chapter descriptor found with name {}'.format(chapter))
chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter) chapter_module = course_module.get_child_by(lambda m: m.url_name == chapter)
if chapter_module is None: if chapter_module is None:
...@@ -251,7 +321,20 @@ def index(request, course_id, chapter=None, section=None, ...@@ -251,7 +321,20 @@ def index(request, course_id, chapter=None, section=None,
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache) instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
save_child_position(chapter_module, section, instance_module) save_child_position(chapter_module, section, instance_module)
# check here if this section *is* a timed module.
if section_module.category == 'timelimit':
timer_context = update_timelimit_module(request.user, course_id, student_module_cache,
section_descriptor, section_module)
if 'timer_expiration_datetime' in timer_context:
context.update(timer_context)
else:
# if there is no expiration defined, then we know the timer has expired:
return HttpResponseRedirect(timer_context['time_expired_redirect_url'])
else:
# check here if this page is within a course that has an active timed module running. If so, then
# add in the appropriate timer information to the rendering context:
context.update(check_for_active_timelimit_module(request, course_id, course))
context['content'] = section_module.get_html() context['content'] = section_module.get_html()
else: else:
# section is none, so display a message # section is none, so display a message
......
...@@ -238,8 +238,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None ...@@ -238,8 +238,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
###### PMathML input ###### ###### PMathML input ######
# convert mathml answer to formula # convert mathml answer to formula
try: try:
if dynamath: mmlans = dynamath[0] if dynamath else None
mmlans = dynamath[0]
except Exception, err: except Exception, err:
mmlans = None mmlans = None
if not mmlans: if not mmlans:
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
@import 'course/courseware/sidebar'; @import 'course/courseware/sidebar';
@import 'course/courseware/amplifier'; @import 'course/courseware/amplifier';
@import 'course/layout/calculator'; @import 'course/layout/calculator';
@import 'course/layout/timer';
// course-specific courseware (all styles in these files should be gated by a // course-specific courseware (all styles in these files should be gated by a
// course-specific class). This should be replaced with a better way of // course-specific class). This should be replaced with a better way of
......
div.timer-main {
position: fixed;
z-index: 99;
top: 0;
right: 0;
width: 100%;
border-top: 2px solid #000;
div#timer_wrapper {
position: absolute;
top: -3px;
right: 10px;
background: #000;
color: #fff;
padding: 10px 20px;
border-radius: 3px;
}
.timer_return_url {
display: block;
margin-bottom: 5px;
border-bottom: 1px solid tint(#000, 20%);
padding-bottom: 5px;
font-size: 13px;
}
.timer_label {
color: #b0b0b0;
font-size: 13px;
margin-bottom: 3px;
}
#exam_timer {
font-weight: bold;
font-size: 15px;
letter-spacing: 1px;
}
}
...@@ -59,12 +59,75 @@ ...@@ -59,12 +59,75 @@
}); });
}); });
</script> </script>
% if timer_expiration_datetime:
<script type="text/javascript">
var timer = {
timer_inst : null,
get_remaining_secs : function() {
// set the end time when the template is rendered.
// This value should be UTC time as number of milliseconds since epoch.
var endTime = new Date(${timer_expiration_datetime});
var currentTime = new Date();
var remaining_secs = Math.floor((endTime - currentTime)/1000);
return remaining_secs;
},
get_time_string : function() {
function pretty_time_string(num) {
return ( num < 10 ? "0" : "" ) + num;
}
// count down in terms of hours, minutes, and seconds:
var hours = pretty_time_string(Math.floor(remaining_secs / 3600));
remaining_secs = remaining_secs % 3600;
var minutes = pretty_time_string(Math.floor(remaining_secs / 60));
remaining_secs = remaining_secs % 60;
var seconds = pretty_time_string(Math.floor(remaining_secs));
var remainingTimeString = hours + ":" + minutes + ":" + seconds;
return remainingTimeString;
},
update_time : function(self) {
remaining_secs = self.get_remaining_secs();
if (remaining_secs <= 0) {
self.end(self);
}
$('#exam_timer').text(self.get_time_string(remaining_secs));
},
start : function() { var that = this;
this.timer_inst = setInterval(function(){ that.update_time(that); }, 1000);
},
end : function(self) {
clearInterval(self.timer_inst);
// redirect to specified URL:
window.location = "${time_expired_redirect_url}";
}
}
// start timer right away:
timer.start();
</script>
% endif
</%block> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> % if timer_expiration_datetime:
<div class="timer-main">
<div id="timer_wrapper">
% if timer_navigation_return_url:
<a href="${timer_navigation_return_url}" class="timer_return_url">Return to Exam</a>
% endif
<div class="timer_label">Time Remaining:</div> <div id="exam_timer" class="timer_value">&nbsp;</div>
</div>
</div>
% endif
% if accordion:
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
% endif
<section class="container"> <section class="container">
<div class="course-wrapper"> <div class="course-wrapper">
% if accordion:
<section aria-label="Course Navigation" class="course-index"> <section aria-label="Course Navigation" class="course-index">
<header id="open_close_accordion"> <header id="open_close_accordion">
<a href="#">close</a> <a href="#">close</a>
...@@ -76,6 +139,7 @@ ...@@ -76,6 +139,7 @@
</nav> </nav>
</div> </div>
</section> </section>
% endif
<section class="course-content"> <section class="course-content">
${content} ${content}
......
...@@ -29,13 +29,18 @@ ...@@ -29,13 +29,18 @@
<body class="<%block name='bodyclass'/>"> <body class="<%block name='bodyclass'/>">
% if not suppress_toplevel_navigation:
<%include file="navigation.html" /> <%include file="navigation.html" />
% endif
<section class="content-wrapper"> <section class="content-wrapper">
${self.body()} ${self.body()}
<%block name="bodyextra"/> <%block name="bodyextra"/>
</section> </section>
% if not suppress_toplevel_navigation:
<%include file="footer.html" /> <%include file="footer.html" />
% endif
<%static:js group='application'/> <%static:js group='application'/>
<%static:js group='module-js'/> <%static:js group='module-js'/>
......
${module_content} ${module_content}
%if edit_link: %if location.category in ['problem','video','html']:
% if edit_link:
<div> <div>
<a href="${edit_link}">Edit</a> / <a href="${edit_link}">Edit</a> /
<a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', { <a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
...@@ -9,7 +10,7 @@ ${module_content} ...@@ -9,7 +10,7 @@ ${module_content}
'user': '${user}' 'user': '${user}'
})" id="${element_id}_xqa_log">QA</a> })" id="${element_id}_xqa_log">QA</a>
</div> </div>
% endif % endif
<div><a href="#${element_id}_debug" id="${element_id}_trig">Staff Debug Info</a></div> <div><a href="#${element_id}_debug" id="${element_id}_trig">Staff Debug Info</a></div>
<section id="${element_id}_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto" > <section id="${element_id}_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto" >
...@@ -74,3 +75,5 @@ category = ${category | h} ...@@ -74,3 +75,5 @@ category = ${category | h}
'user': '${user}' 'user': '${user}'
}); });
</script> </script>
%endif
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