Commit 793bbfd3 by Calen Pennington

Set up dev environment for testing xml vs mongo vs split_mongo modulestores

parents bda46c42 dff0c10b
...@@ -9,6 +9,7 @@ gfortran ...@@ -9,6 +9,7 @@ gfortran
liblapack-dev liblapack-dev
libfreetype6-dev libfreetype6-dev
libpng12-dev libpng12-dev
libjpeg-dev
libxml2-dev libxml2-dev
libxslt-dev libxslt-dev
yui-compressor yui-compressor
......
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.data data = module.data
if rewrite_static_links: if rewrite_static_links:
data = replace_static_urls( data = replace_static_urls(
module.data, module.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(),
...@@ -41,7 +39,6 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links= ...@@ -41,7 +39,6 @@ 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)
...@@ -55,7 +52,6 @@ def set_module_info(store, location, post_data): ...@@ -55,7 +52,6 @@ def set_module_info(store, location, post_data):
# 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']
......
import json import json
import shutil import shutil
from django.test.client import Client from django.test.client import Client
from override_settings import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from path import path from path import path
...@@ -10,6 +10,7 @@ import json ...@@ -10,6 +10,7 @@ import json
from fs.osfs import OSFS from fs.osfs import OSFS
import copy import copy
from mock import Mock from mock import Mock
from json import dumps, loads
from student.models import Registration from student.models import Registration
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -207,6 +208,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -207,6 +208,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check for custom_tags # check for custom_tags
self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template') self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
# check for graiding_policy.json
fs = OSFS(root_dir / 'test_export/policies/6.002_Spring_2012')
self.assertTrue(fs.exists('grading_policy.json'))
# compare what's on disk compared to what we have in our course
with fs.open('grading_policy.json','r') as grading_policy:
on_disk = loads(grading_policy.read())
course = ms.get_item(location)
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
# remove old course # remove old course
delete_course(ms, cs, location) delete_course(ms, cs, location)
......
...@@ -249,8 +249,7 @@ class CourseGradingTest(CourseTestCase): ...@@ -249,8 +249,7 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D") self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "cutoff add D")
test_grader.grace_period = {'hours': '4'} test_grader.grace_period = {'hours': 4, 'minutes': 5, 'seconds': 0}
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__) altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
print test_grader.__dict__ print test_grader.__dict__
print altered_grader.__dict__ print altered_grader.__dict__
......
import json import json
import shutil import shutil
from django.test.client import Client from django.test.client import Client
from override_settings import override_settings
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from path import path from path import path
......
...@@ -2,7 +2,6 @@ import json ...@@ -2,7 +2,6 @@ import json
import copy import copy
from time import time from time import time
from django.test import TestCase from django.test import TestCase
from override_settings import override_settings
from django.conf import settings from django.conf import settings
from student.models import Registration from student.models import Registration
......
...@@ -126,7 +126,8 @@ def index(request): ...@@ -126,7 +126,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
}) })
...@@ -1254,6 +1255,10 @@ def edge(request): ...@@ -1254,6 +1255,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
......
...@@ -156,13 +156,8 @@ class CourseGradingModel(object): ...@@ -156,13 +156,8 @@ class CourseGradingModel(object):
if 'grace_period' in graceperiodjson: if 'grace_period' in graceperiodjson:
graceperiodjson = graceperiodjson['grace_period'] graceperiodjson = graceperiodjson['grace_period']
timedelta_kwargs = dict( # lms requires these to be in a fixed order
(key, float(val)) grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
for key, val
in graceperiodjson.items()
if key in ('days', 'seconds', 'minutes', 'hours')
)
grace_rep = timedelta(**timedelta_kwargs)
descriptor = get_modulestore(course_location).get_item(course_location) descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.lms.graceperiod = grace_rep descriptor.lms.graceperiod = grace_rep
...@@ -241,6 +236,7 @@ class CourseGradingModel(object): ...@@ -241,6 +236,7 @@ class CourseGradingModel(object):
@staticmethod @staticmethod
def convert_set_grace_period(descriptor): def convert_set_grace_period(descriptor):
<<<<<<< HEAD
# 5 hours 59 minutes 59 seconds => converted to iso format # 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod rawgrace = descriptor.lms.graceperiod
if rawgrace: if rawgrace:
...@@ -265,6 +261,14 @@ class CourseGradingModel(object): ...@@ -265,6 +261,14 @@ class CourseGradingModel(object):
return graceperiod return graceperiod
else: else:
return None return None
=======
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
rawgrace = descriptor.metadata.get('graceperiod', None)
if rawgrace:
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
return parsedgrace
else: return None
>>>>>>> origin/master
@staticmethod @staticmethod
def parse_grader(json_grader): def parse_grader(json_grader):
......
...@@ -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
......
...@@ -58,6 +58,9 @@ $(document).ready(function() { ...@@ -58,6 +58,9 @@ $(document).ready(function() {
drop: onSectionReordered, drop: onSectionReordered,
greedy: true greedy: true
}); });
// stop clicks on drag bars from doing their thing w/o stopping drag
$('.drag-handle').click(function(e) {e.preventDefault(); });
}); });
...@@ -202,13 +205,17 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) { ...@@ -202,13 +205,17 @@ function _handleReorder(event, ui, parentIdField, childrenSelector) {
children = _.without(children, ui.draggable.data('id')); children = _.without(children, ui.draggable.data('id'));
} }
// add to this parent (figure out where) // add to this parent (figure out where)
for (var i = 0; i < _els.length; i++) { for (var i = 0, bump = 0; i < _els.length; i++) {
if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) { if (ui.draggable.is(_els[i])) {
bump = -1; // bump indicates that the draggable was passed in the dom but not children's list b/c
// it's not in that list
}
else if (ui.offset.top < $(_els[i]).offset().top) {
// insert at i in children and _els // insert at i in children and _els
ui.draggable.insertBefore($(_els[i])); ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below) // TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;"); ui.draggable.attr("style", "position:relative;");
children.splice(i, 0, ui.draggable.data('id')); children.splice(i + bump, 0, ui.draggable.data('id'));
break; break;
} }
} }
......
...@@ -227,7 +227,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({ ...@@ -227,7 +227,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
time = 0; time = 0;
} }
var newVal = new Date(date.getTime() + time * 1000); var newVal = new Date(date.getTime() + time * 1000);
if (cacheModel.get(fieldName).getTime() !== newVal.getTime()) { if (!cacheModel.has(fieldName) || cacheModel.get(fieldName).getTime() !== newVal.getTime()) {
cacheModel.save(fieldName, newVal, { error: CMS.ServerError}); cacheModel.save(fieldName, newVal, { error: CMS.ServerError});
} }
} }
......
...@@ -107,6 +107,8 @@ ...@@ -107,6 +107,8 @@
<script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script> <script type="text/javascript" src="${static.url('js/models/course_relative.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script> <script type="text/javascript" src="${static.url('js/views/grader-select-view.js')}"></script>
<script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script> <script type="text/javascript" src="${static.url('js/models/settings/course_grading_policy.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/overview.js')}"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
......
...@@ -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">
......
...@@ -2,7 +2,7 @@ import django.test ...@@ -2,7 +2,7 @@ import django.test
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from override_settings import override_settings from django.test.utils import override_settings
from course_groups.models import CourseUserGroup from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts, from course_groups.cohorts import (get_cohort, get_course_cohorts,
......
...@@ -13,12 +13,18 @@ log = logging.getLogger(__name__) ...@@ -13,12 +13,18 @@ log = logging.getLogger(__name__)
def _url_replace_regex(prefix): def _url_replace_regex(prefix):
"""
Match static urls in quotes that don't end in '?raw'.
To anyone contemplating making this more complicated:
http://xkcd.com/1171/
"""
return r""" return r"""
(?x) # flags=re.VERBOSE (?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes (?P<quote>\\?['"]) # the opening quotes
(?P<prefix>{prefix}) # theeprefix (?P<prefix>{prefix}) # the prefix
(?P<rest>.*?) # everything else in the url (?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote (?P=quote) # the first matching closing quote
""".format(prefix=prefix) """.format(prefix=prefix)
...@@ -74,12 +80,20 @@ def replace_static_urls(text, data_directory, course_namespace=None): ...@@ -74,12 +80,20 @@ def replace_static_urls(text, data_directory, course_namespace=None):
quote = match.group('quote') quote = match.group('quote')
rest = match.group('rest') rest = match.group('rest')
# Don't mess with things that end in '?raw'
if rest.endswith('?raw'):
return original
# course_namespace is not None, then use studio style urls # course_namespace is not None, then use studio style urls
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore): if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
url = StaticContent.convert_legacy_static_url(rest, course_namespace) url = StaticContent.convert_legacy_static_url(rest, course_namespace)
# In debug mode, if we can find the url as is,
elif settings.DEBUG and finders.find(rest, True):
return original
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
else: else:
course_path = "/".join((data_directory, rest)) course_path = "/".join((data_directory, rest))
try: try:
if staticfiles_storage.exists(rest): if staticfiles_storage.exists(rest):
url = staticfiles_storage.url(rest) url = staticfiles_storage.url(rest)
......
from nose.tools import assert_equals import re
from static_replace import replace_static_urls, replace_course_urls
from nose.tools import assert_equals, assert_true, assert_false
from static_replace import (replace_static_urls, replace_course_urls,
_url_replace_regex)
from mock import patch, Mock from mock import patch, Mock
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
...@@ -75,3 +78,34 @@ def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings): ...@@ -75,3 +78,34 @@ def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings):
mock_storage.exists.return_value = False mock_storage.exists.return_value = False
assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY)) assert_equals('"/static/data_dir/file.png"', replace_static_urls(STATIC_SOURCE, DATA_DIRECTORY))
def test_raw_static_check():
"""
Make sure replace_static_urls leaves alone things that end in '.raw'
"""
path = '"/static/foo.png?raw"'
assert_equals(path, replace_static_urls(path, DATA_DIRECTORY))
text = 'text <tag a="/static/js/capa/protex/protex.nocache.js?raw"/><div class="'
assert_equals(path, replace_static_urls(path, text))
def test_regex():
yes = ('"/static/foo.png"',
'"/static/foo.png"',
"'/static/foo.png'")
no = ('"/not-static/foo.png"',
'"/static/foo', # no matching quote
)
regex = _url_replace_regex('/static/')
for s in yes:
print 'Should match: {0!r}'.format(s)
assert_true(re.match(regex, s))
for s in no:
print 'Should not match: {0!r}'.format(s)
assert_false(re.match(regex, s))
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
import os import os
from override_settings import override_settings from django.test.utils import override_settings
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from status import get_site_status_msg from status import get_site_status_msg
......
...@@ -18,8 +18,9 @@ def jsdate_to_time(field): ...@@ -18,8 +18,9 @@ def jsdate_to_time(field):
""" """
if field is None: if field is None:
return field return field
elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z elif isinstance(field, basestring):
d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable # ISO format but ignores time zone assuming it's Z.
d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
return d.utctimetuple() return d.utctimetuple()
elif isinstance(field, (int, long, float)): elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000) return time.gmtime(field / 1000)
......
...@@ -632,8 +632,12 @@ class MultipleChoiceResponse(LoncapaResponse): ...@@ -632,8 +632,12 @@ class MultipleChoiceResponse(LoncapaResponse):
# define correct choices (after calling secondary setup) # define correct choices (after calling secondary setup)
xml = self.xml xml = self.xml
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id')) cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id'))
self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml] # contextualize correct attribute and then select ones for which
# correct = "true"
self.correct_choices = [contextualize_text(choice.get('name'), self.context)
for choice in cxml
if contextualize_text(choice.get('correct'), self.context) == "true"]
def mc_setup_response(self): def mc_setup_response(self):
''' '''
...@@ -999,7 +1003,7 @@ def sympy_check2(): ...@@ -999,7 +1003,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",
......
...@@ -510,7 +510,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -510,7 +510,7 @@ class CourseDescriptor(SequenceDescriptor):
# 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): def to_datetime(timestamp):
return datetime.fromtimestamp(time.mktime(timestamp)) return datetime(*timestamp[:6])
def get_date(field): def get_date(field):
timetuple = self._try_parse_time(field) timetuple = self._try_parse_time(field)
...@@ -660,7 +660,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -660,7 +660,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):
""" """
...@@ -716,6 +716,10 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -716,6 +716,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')
......
...@@ -74,7 +74,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -74,7 +74,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
......
...@@ -2,6 +2,7 @@ import logging ...@@ -2,6 +2,7 @@ import logging
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from fs.osfs import OSFS from fs.osfs import OSFS
from json import dumps
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir): def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
...@@ -27,6 +28,12 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d ...@@ -27,6 +28,12 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
# export the course updates # export the course updates
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html') export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
# export the grading policy
policies_dir = export_fs.makeopendir('policies')
course_run_policy_dir = policies_dir.makeopendir(course.location.name)
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
grading_policy.write(dumps(course.definition['data']['grading_policy']))
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''): def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None) query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
......
...@@ -51,9 +51,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -51,9 +51,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
REQUEST_HINT = 'request_hint' REQUEST_HINT = 'request_hint'
DONE = 'done' DONE = 'done'
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]}
js_module_name = "SelfAssessment"
student_answers = List(scope=Scope.student_state, default=[]) student_answers = List(scope=Scope.student_state, default=[])
scores = List(scope=Scope.student_state, default=[]) scores = List(scope=Scope.student_state, default=[])
hints = List(scope=Scope.student_state, default=[]) hints = List(scope=Scope.student_state, default=[])
......
...@@ -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
from xblock.core import Float, String, Boolean, Scope
log = logging.getLogger(__name__)
class TimeLimitModule(XModule):
'''
Wrapper module which imposes a time constraint for the completion of its child.
'''
beginning_at = Float(help="The time this timer was started", scope=Scope.student_state)
ending_at = Float(help="The time this timer will end", scope=Scope.student_state)
accomodation_code = String(help="A code indicating accommodations to be given the student", scope=Scope.student_state)
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)
suppress_toplevel_navigation = Boolean(help="Whether the toplevel navigation should be suppressed when viewing this module", scope=Scope.settings)
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
self.rendered = False
# 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_remaining_time_in_ms(self):
return int((self.ending_at - time()) * 1000)
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
...@@ -50,7 +50,7 @@ if Backbone? ...@@ -50,7 +50,7 @@ if Backbone?
convertMath: -> convertMath: ->
element = @$(".post-body") element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
renderResponses: -> renderResponses: ->
......
...@@ -47,7 +47,7 @@ if Backbone? ...@@ -47,7 +47,7 @@ if Backbone?
convertMath: -> convertMath: ->
element = @$(".post-body") element = @$(".post-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
toggleVote: (event) -> toggleVote: (event) ->
......
...@@ -26,7 +26,7 @@ if Backbone? ...@@ -26,7 +26,7 @@ if Backbone?
convertMath: -> convertMath: ->
body = @$el.find(".response-body") body = @$el.find(".response-body")
body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.html() body.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight body.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]] MathJax.Hub.Queue ["Typeset", MathJax.Hub, body[0]]
markAsStaff: -> markAsStaff: ->
......
...@@ -30,7 +30,7 @@ if Backbone? ...@@ -30,7 +30,7 @@ if Backbone?
convertMath: -> convertMath: ->
element = @$(".response-body") element = @$(".response-body")
element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.html() element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text()
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]]
markAsStaff: -> markAsStaff: ->
......
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from override_settings import override_settings from django.test.utils import override_settings
import xmodule.modulestore.django import xmodule.modulestore.django
......
...@@ -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)
......
...@@ -11,7 +11,7 @@ from django.test import TestCase ...@@ -11,7 +11,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from override_settings import override_settings from django.test.utils import override_settings
import xmodule.modulestore.django import xmodule.modulestore.django
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
......
...@@ -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
...@@ -21,8 +21,8 @@ from courseware.courses import (get_courses, get_course_with_access, ...@@ -21,8 +21,8 @@ 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.model_data import ModelDataCache from courseware.model_data import ModelDataCache
from module_render import toc_for_course, get_module from module_render import toc_for_course, get_module_for_descriptor, get_module
from student.models import UserProfile from courseware.models import StudentModule
from django_comment_client.utils import get_discussion_title from django_comment_client.utils import get_discussion_title
...@@ -40,7 +40,6 @@ log = logging.getLogger("mitx.courseware") ...@@ -40,7 +40,6 @@ log = logging.getLogger("mitx.courseware")
template_imports = {'urllib': urllib} template_imports = {'urllib': urllib}
def user_groups(user): def user_groups(user):
""" """
TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately. TODO (vshnayder): This is not used. When we have a new plan for groups, adjust appropriately.
...@@ -161,6 +160,71 @@ def save_child_position(seq_module, child_name): ...@@ -161,6 +160,71 @@ def save_child_position(seq_module, child_name):
seq_module.position = position seq_module.position = position
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 = {}
# TODO (cpennington): Once we can query the course structure, replace this with such a query
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 = ModelDataCache.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 timelimit_descriptor.time_expired_redirect_url is None:
raise Http404("no time_expired_redirect_url specified at this location: {} ".format(timelimit_module.location))
context['time_expired_redirect_url'] = timelimit_descriptor.time_expired_redirect_url
# Fetch the remaining time relative to the end time as stored in the module when it was started.
# This value should be in milliseconds.
remaining_time = timelimit_module.get_remaining_time_in_ms()
context['timer_expiration_duration'] = remaining_time
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, model_data_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_duration' in dict if timer is still active, and not if timer has expired.
'''
context = {}
# determine where to go when the exam ends:
if timelimit_descriptor.time_expired_redirect_url is None:
raise Http404("No time_expired_redirect_url specified at this location: {} ".format(timelimit_module.location))
context['time_expired_redirect_url'] = timelimit_descriptor.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 timelimit_descriptor.duration is None:
raise Http404("No duration specified at this location: {} ".format(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.
timelimit_module.begin(timelimit_descriptor.duration)
# the exam has been started, either because the student is returning to the
# exam page, or because they have just visited it. Fetch the remaining time relative to the
# end time as stored in the module when it was started.
context['timer_expiration_duration'] = timelimit_module.get_remaining_time_in_ms()
# also use the timed module to determine whether top-level navigation is visible:
context['suppress_toplevel_navigation'] = timelimit_descriptor.suppress_toplevel_navigation
return context
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
...@@ -224,7 +288,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -224,7 +288,7 @@ def index(request, course_id, chapter=None, section=None,
if chapter_descriptor is not None: if chapter_descriptor is not None:
save_child_position(course_module, chapter) save_child_position(course_module, chapter)
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:
...@@ -253,6 +317,20 @@ def index(request, course_id, chapter=None, section=None, ...@@ -253,6 +317,20 @@ def index(request, course_id, chapter=None, section=None,
# Save where we are in the chapter # Save where we are in the chapter
save_child_position(chapter_module, section) save_child_position(chapter_module, section)
# 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_duration' 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
......
...@@ -6,7 +6,7 @@ from django.conf import settings ...@@ -6,7 +6,7 @@ from django.conf import settings
from mock import Mock from mock import Mock
from override_settings import override_settings from django.test.utils import override_settings
import xmodule.modulestore.django import xmodule.modulestore.django
......
...@@ -15,6 +15,8 @@ from django.http import HttpResponse ...@@ -15,6 +15,8 @@ from django.http import HttpResponse
from django.utils import simplejson from django.utils import simplejson
from django_comment_client.models import Role from django_comment_client.models import Role
from django_comment_client.permissions import check_permissions_by_view from django_comment_client.permissions import check_permissions_by_view
from xmodule.modulestore.exceptions import NoPathToItem
from mitxmako import middleware from mitxmako import middleware
import pystache_custom as pystache import pystache_custom as pystache
...@@ -168,6 +170,7 @@ def initialize_discussion_info(course): ...@@ -168,6 +170,7 @@ def initialize_discussion_info(course):
# get all discussion models within this course_id # get all discussion models within this course_id
all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id) all_modules = modulestore().get_items(['i4x', course.location.org, course.location.course, 'discussion', None], course_id=course_id)
path_to_locations = {}
for module in all_modules: for module in all_modules:
skip_module = False skip_module = False
for key in ('id', 'discussion_category', 'for'): for key in ('id', 'discussion_category', 'for'):
...@@ -175,6 +178,14 @@ def initialize_discussion_info(course): ...@@ -175,6 +178,14 @@ def initialize_discussion_info(course):
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location)) log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
skip_module = True skip_module = True
# cdodge: pre-compute the path_to_location. Note this can throw an exception for any
# dangling discussion modules
try:
path_to_locations[module.location] = path_to_location(modulestore(), course.id, module.location)
except NoPathToItem:
log.warning("Could not compute path_to_location for {0}. Perhaps this is an orphaned discussion module?!? Skipping...".format(module.location))
skip_module = True
if skip_module: if skip_module:
continue continue
...@@ -237,6 +248,7 @@ def initialize_discussion_info(course): ...@@ -237,6 +248,7 @@ def initialize_discussion_info(course):
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map _DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map _DISCUSSIONINFO[course.id]['category_map'] = category_map
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now() _DISCUSSIONINFO[course.id]['timestamp'] = datetime.now()
_DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations
class JsonResponse(HttpResponse): class JsonResponse(HttpResponse):
...@@ -392,11 +404,23 @@ def get_courseware_context(content, course): ...@@ -392,11 +404,23 @@ def get_courseware_context(content, course):
if id in id_map: if id in id_map:
location = id_map[id]["location"].url() location = id_map[id]["location"].url()
title = id_map[id]["title"] title = id_map[id]["title"]
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
url = reverse('courseware_position', kwargs={"course_id": course_id, # cdodge: did we pre-compute, if so, then let's use that rather than recomputing
"chapter": chapter, if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']:
"section": section, (course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location]
"position": position}) else:
try:
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, location)
except NoPathToItem:
# Object is not in the graph any longer, let's just get path to the base of the course
# so that we can at least return something to the caller
(course_id, chapter, section, position) = path_to_location(modulestore(), course.id, course.location)
url = reverse('courseware_position', kwargs={"course_id":course_id,
"chapter":chapter,
"section":section,
"position":position})
content_info = {"courseware_url": url, "courseware_title": title} content_info = {"courseware_url": url, "courseware_title": title}
return content_info return content_info
......
...@@ -15,7 +15,7 @@ import json ...@@ -15,7 +15,7 @@ import json
from nose import SkipTest from nose import SkipTest
from mock import patch, Mock from mock import patch, Mock
from override_settings import override_settings from django.test.utils import override_settings
# Need access to internal func to put users in the right group # Need access to internal func to put users in the right group
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
......
...@@ -386,6 +386,46 @@ def instructor_dashboard(request, course_id): ...@@ -386,6 +386,46 @@ def instructor_dashboard(request, course_id):
track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard') track.views.server_track(request, 'remove-instructor {0}'.format(user), {}, page='idashboard')
#---------------------------------------- #----------------------------------------
# DataDump
elif 'Download CSV of all student profile data' in action:
enrolled_students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username').select_related("profile")
profkeys = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education',
'mailing_address', 'goals']
datatable = {'header': ['username', 'email'] + profkeys}
def getdat(u):
p = u.profile
return [u.username, u.email] + [getattr(p,x,'') for x in profkeys]
datatable['data'] = [getdat(u) for u in enrolled_students]
datatable['title'] = 'Student profile data for course %s' % course_id
return return_csv('profiledata_%s.csv' % course_id, datatable)
elif 'Download CSV of all responses to problem' in action:
problem_to_dump = request.POST.get('problem_to_dump','')
if problem_to_dump[-4:]==".xml":
problem_to_dump=problem_to_dump[:-4]
try:
(org, course_name, run)=course_id.split("/")
module_state_key="i4x://"+org+"/"+course_name+"/problem/"+problem_to_dump
smdat = StudentModule.objects.filter(course_id=course_id,
module_state_key=module_state_key)
smdat = smdat.order_by('student')
msg+="Found module to reset. "
except Exception as err:
msg+="<font color='red'>Couldn't find module with that urlname. </font>"
msg += "<pre>%s</pre>" % escape(err)
smdat = []
if smdat:
datatable = {'header': ['username', 'state']}
datatable['data'] = [ [x.student.username, x.state] for x in smdat ]
datatable['title'] = 'Student state for problem %s' % problem_to_dump
return return_csv('student_state_from_%s.csv' % problem_to_dump, datatable)
#----------------------------------------
# Group management # Group management
elif 'List beta testers' in action: elif 'List beta testers' in action:
......
...@@ -22,7 +22,7 @@ from mitxmako.shortcuts import render_to_string ...@@ -22,7 +22,7 @@ from mitxmako.shortcuts import render_to_string
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
from override_settings import override_settings from django.test.utils import override_settings
from django.http import QueryDict from django.http import QueryDict
......
...@@ -53,7 +53,6 @@ with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file: ...@@ -53,7 +53,6 @@ with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
SITE_NAME = ENV_TOKENS['SITE_NAME'] SITE_NAME = ENV_TOKENS['SITE_NAME']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN') SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
CSRF_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
BOOK_URL = ENV_TOKENS['BOOK_URL'] BOOK_URL = ENV_TOKENS['BOOK_URL']
MEDIA_URL = ENV_TOKENS['MEDIA_URL'] MEDIA_URL = ENV_TOKENS['MEDIA_URL']
......
...@@ -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
......
...@@ -67,7 +67,7 @@ section.course-index { ...@@ -67,7 +67,7 @@ section.course-index {
} }
.chapter { .chapter {
width: 100%; width: 100% !important;
@include box-sizing(border-box); @include box-sizing(border-box);
padding: 11px 14px; padding: 11px 14px;
@include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0)); @include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0));
...@@ -99,6 +99,8 @@ section.course-index { ...@@ -99,6 +99,8 @@ section.course-index {
@include border-radius(0); @include border-radius(0);
margin: 0; margin: 0;
padding: 9px 0 9px 9px; padding: 9px 0 9px 9px;
overflow: auto;
width: 100%;
li { li {
border-bottom: 0; border-bottom: 0;
......
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,76 @@ ...@@ -59,12 +59,76 @@
}); });
}); });
</script> </script>
% if timer_expiration_duration:
<script type="text/javascript">
var timer = {
timer_inst : null,
end_time : null,
get_remaining_secs : function(endTime) {
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(self.end_time);
if (remaining_secs <= 0) {
self.end(self);
}
$('#exam_timer').text(self.get_time_string(remaining_secs));
},
start : function() { var that = this;
// set the end time when the template is rendered.
// This value should be UTC time as number of milliseconds since epoch.
this.end_time = new Date((new Date()).getTime() + ${timer_expiration_duration});
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_duration:
<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 +140,7 @@ ...@@ -76,6 +140,7 @@
</nav> </nav>
</div> </div>
</section> </section>
% endif
<section class="course-content"> <section class="course-content">
${content} ${content}
......
...@@ -64,6 +64,7 @@ function goto( mode) ...@@ -64,6 +64,7 @@ function goto( mode)
<a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> | <a href="#" onclick="goto('Admin');" class="${modeflag.get('Admin')}">Admin</a> |
<a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> | <a href="#" onclick="goto('Forum Admin');" class="${modeflag.get('Forum Admin')}">Forum Admin</a> |
<a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a> | <a href="#" onclick="goto('Enrollment');" class="${modeflag.get('Enrollment')}">Enrollment</a> |
<a href="#" onclick="goto('Data');" class="${modeflag.get('Data')}">DataDump</a> |
<a href="#" onclick="goto('Manage Groups');" class="${modeflag.get('Manage Groups')}">Manage Groups</a> ] <a href="#" onclick="goto('Manage Groups');" class="${modeflag.get('Manage Groups')}">Manage Groups</a> ]
</h2> </h2>
...@@ -269,6 +270,20 @@ function goto( mode) ...@@ -269,6 +270,20 @@ function goto( mode)
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
%if modeflag.get('Data'):
<hr width="40%" style="align:left">
<p>
<input type="submit" name="action" value="Download CSV of all student profile data">
</p>
<p> Problem urlname:
<input type="text" name="problem_to_dump" size="40">
<input type="submit" name="action" value="Download CSV of all responses to problem">
</p>
<hr width="40%" style="align:left">
%endif
##-----------------------------------------------------------------------------
%if modeflag.get('Manage Groups'): %if modeflag.get('Manage Groups'):
%if instructor_access: %if instructor_access:
<hr width="40%" style="align:left"> <hr width="40%" style="align:left">
......
...@@ -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" >
...@@ -82,3 +83,5 @@ category = ${category | h} ...@@ -82,3 +83,5 @@ category = ${category | h}
); );
}); });
</script> </script>
%endif
...@@ -24,7 +24,6 @@ django_nose ...@@ -24,7 +24,6 @@ django_nose
nosexcover==1.0.7 nosexcover==1.0.7
rednose==0.3.3 rednose==0.3.3
GitPython==0.3.2.RC1 GitPython==0.3.2.RC1
django-override-settings==1.2
mock==0.8.0 mock==0.8.0
PyYAML==3.10 PyYAML==3.10
South==0.7.6 South==0.7.6
......
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