Commit 7b118430 by Diana Huang

Merge branch 'master' into diana/click-tracking

parents 125e0b21 bfcc0559
......@@ -9,6 +9,7 @@ gfortran
liblapack-dev
libfreetype6-dev
libpng12-dev
libjpeg-dev
libxml2-dev
libxslt-dev
yui-compressor
......
import logging
from static_replace import replace_static_urls
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from lxml import etree
import re
from django.http import HttpResponseBadRequest, Http404
from django.http import Http404
def get_module_info(store, location, parent_location=None, rewrite_static_links=False):
try:
if location.revision is None:
module = store.get_item(location)
else:
module = store.get_item(location)
except ItemNotFoundError:
raise Http404
try:
if location.revision is None:
module = store.get_item(location)
else:
module = store.get_item(location)
except ItemNotFoundError:
# create a new one
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
data = module.definition['data']
if rewrite_static_links:
data = replace_static_urls(
module.definition['data'],
None,
course_namespace=Location([
module.location.tag,
module.location.org,
module.location.course,
data = module.definition['data']
if rewrite_static_links:
data = replace_static_urls(
module.definition['data'],
None,
None
])
)
course_namespace=Location([
module.location.tag,
module.location.org,
module.location.course,
None,
None
])
)
return {
return {
'id': module.location.url(),
'data': data,
'metadata': module.metadata
......@@ -39,58 +37,56 @@ def get_module_info(store, location, parent_location=None, rewrite_static_links=
def set_module_info(store, location, post_data):
module = None
isNew = False
try:
if location.revision is None:
module = store.get_item(location)
else:
module = store.get_item(location)
except:
pass
module = None
try:
if location.revision is None:
module = store.get_item(location)
else:
module = store.get_item(location)
except:
pass
if module is None:
# new module at this location
# presume that we have an 'Empty' template
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
isNew = True
if module is None:
# new module at this location
# presume that we have an 'Empty' template
template_location = Location(['i4x', 'edx', 'templates', location.category, 'Empty'])
module = store.clone_item(template_location, location)
if post_data.get('data') is not None:
data = post_data['data']
store.update_item(location, data)
if post_data.get('data') is not None:
data = post_data['data']
store.update_item(location, data)
# 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
# deleting the children object from the children collection
if 'children' in post_data and post_data['children'] is not None:
children = post_data['children']
store.update_children(location, children)
# 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
# deleting the children object from the children collection
if 'children' in post_data and post_data['children'] is not None:
children = post_data['children']
store.update_children(location, children)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# 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
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
# 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'
for metadata_key in posted_metadata.keys():
# 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
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata:
del module.metadata[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
module.metadata.update(posted_metadata)
# commit to datastore
store.update_metadata(location, module.metadata)
# cdodge: also commit any metadata which might have been passed along in the
# POST from the client, if it is there
# 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
# 'apply' the submitted metadata, so we don't end up deleting system metadata
if post_data.get('metadata') is not None:
posted_metadata = post_data['metadata']
# 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'
for metadata_key in posted_metadata.keys():
# 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
if metadata_key in module.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in module.metadata:
del module.metadata[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
module.metadata.update(posted_metadata)
# commit to datastore
store.update_metadata(location, module.metadata)
import json
import shutil
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.core.urlresolvers import reverse
from path import path
......@@ -10,6 +10,7 @@ import json
from fs.osfs import OSFS
import copy
from mock import Mock
from json import dumps, loads
from student.models import Registration
from django.contrib.auth.models import User
......@@ -26,10 +27,12 @@ from xmodule.contentstore.django import contentstore
from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.templates import update_templates
from xmodule.capa_module import CapaDescriptor
from xmodule.course_module import CourseDescriptor
from xmodule.seq_module import SequenceDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
......@@ -207,6 +210,24 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# check for custom_tags
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'))
course = ms.get_item(location)
# 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())
self.assertEqual(on_disk, course.definition['data']['grading_policy'])
#check for policy.json
self.assertTrue(fs.exists('policy.json'))
# compare what's on disk to what we have in the course module
with fs.open('policy.json','r') as course_policy:
on_disk = loads(course_policy.read())
self.assertIn('course/6.002_Spring_2012', on_disk)
self.assertEqual(on_disk['course/6.002_Spring_2012'], course.metadata)
# remove old course
delete_course(ms, cs, location)
......@@ -399,3 +420,32 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertIn('markdown', context, "markdown is missing from context")
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
class TemplateTestCase(ModuleStoreTestCase):
def test_template_cleanup(self):
ms = modulestore('direct')
# insert a bogus template in the store
bogus_template_location = Location('i4x', 'edx', 'templates', 'html', 'bogus')
source_template_location = Location('i4x', 'edx', 'templates', 'html', 'Empty')
ms.clone_item(source_template_location, bogus_template_location)
verify_create = ms.get_item(bogus_template_location)
self.assertIsNotNone(verify_create)
# now run cleanup
update_templates()
# now try to find dangling template, it should not be in DB any longer
asserted = False
try:
verify_create = ms.get_item(bogus_template_location)
except ItemNotFoundError:
asserted = True
self.assertTrue(asserted)
......@@ -249,7 +249,7 @@ class CourseGradingTest(CourseTestCase):
altered_grader = CourseGradingModel.update_from_json(test_grader.__dict__)
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__)
self.assertDictEqual(test_grader.__dict__, altered_grader.__dict__, "4 hour grace period")
......
import json
import shutil
from django.test.client import Client
from override_settings import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from path import path
......@@ -86,7 +85,6 @@ class ContentStoreTestCase(ModuleStoreTestCase):
# Now make sure that the user is now actually activated
self.assertTrue(user(email).is_active)
class AuthTestCase(ContentStoreTestCase):
"""Check that various permissions-related things work"""
......
......@@ -2,7 +2,6 @@ import json
import copy
from time import time
from django.test import TestCase
from override_settings import override_settings
from django.conf import settings
from student.models import Registration
......
......@@ -122,7 +122,8 @@ def index(request):
course.location.course,
course.location.name]))
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):
@login_required
@expect_json
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
# 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
......
......@@ -155,7 +155,8 @@ class CourseGradingModel(object):
if 'grace_period' in graceperiodjson:
graceperiodjson = graceperiodjson['grace_period']
grace_rep = " ".join(["%s %s" % (value, key) for (key, value) in graceperiodjson.iteritems()])
# lms requires these to be in a fixed order
grace_rep = "{0[hours]:d} hours {0[minutes]:d} minutes {0[seconds]:d} seconds".format(graceperiodjson)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.metadata['graceperiod'] = grace_rep
......@@ -234,10 +235,10 @@ class CourseGradingModel(object):
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format
# 5 hours 59 minutes 59 seconds => { hours: 5, minutes : 59, seconds : 59}
rawgrace = descriptor.metadata.get('graceperiod', None)
if rawgrace:
parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
parsedgrace = {str(key): int(val) for (val, key) in re.findall('\s*(\d+)\s*(\w+)', rawgrace)}
return parsedgrace
else: return None
......
......@@ -165,13 +165,6 @@ STATICFILES_DIRS = [
# This is how you would use the textbook images locally
# ("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
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
......
......@@ -58,6 +58,9 @@ $(document).ready(function() {
drop: onSectionReordered,
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) {
children = _.without(children, ui.draggable.data('id'));
}
// add to this parent (figure out where)
for (var i = 0; i < _els.length; i++) {
if (!ui.draggable.is(_els[i]) && ui.offset.top < $(_els[i]).offset().top) {
for (var i = 0, bump = 0; i < _els.length; i++) {
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
ui.draggable.insertBefore($(_els[i]));
// TODO figure out correct way to have it remove the style: top:n; setting (and similar line below)
ui.draggable.attr("style", "position:relative;");
children.splice(i, 0, ui.draggable.data('id'));
children.splice(i + bump, 0, ui.draggable.data('id'));
break;
}
}
......
......@@ -227,7 +227,7 @@ CMS.Views.Settings.Details = CMS.Views.ValidatingView.extend({
time = 0;
}
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});
}
}
......
......@@ -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/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/views/overview.js')}"></script>
<script type="text/javascript">
$(document).ready(function() {
......
......@@ -37,7 +37,9 @@
<h1>My Courses</h1>
<article class="my-classes">
% 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">
%for course, url in courses:
<li>
......
......@@ -97,7 +97,7 @@
$cancelButton.bind('click', hideNewUserForm);
$('.new-user-button').bind('click', showNewUserForm);
$body.bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
$('body').bind('keyup', { $cancelButton: $cancelButton }, checkForCancel);
$('.remove-user').click(function() {
$.ajax({
......
......@@ -206,7 +206,7 @@ from contentstore import utils
<section class="setting-details-marketing">
<header>
<h3>Introducing Your Course</h3>
<span class="detail">Information for perspective students</span>
<span class="detail">Information for prospective students</span>
</header>
<div class="row row-col2">
......
......@@ -2,7 +2,7 @@ import django.test
from django.contrib.auth.models import User
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.cohorts import (get_cohort, get_course_cohorts,
......
......@@ -13,12 +13,18 @@ log = logging.getLogger(__name__)
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"""
(?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes
(?P<prefix>{prefix}) # theeprefix
(?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote
(?x) # flags=re.VERBOSE
(?P<quote>\\?['"]) # the opening quotes
(?P<prefix>{prefix}) # the prefix
(?P<rest>.*?) # everything else in the url
(?P=quote) # the first matching closing quote
""".format(prefix=prefix)
......@@ -74,12 +80,20 @@ def replace_static_urls(text, data_directory, course_namespace=None):
quote = match.group('quote')
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
if course_namespace is not None and not isinstance(modulestore(), XMLModuleStore):
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
else:
course_path = "/".join((data_directory, rest))
try:
if staticfiles_storage.exists(rest):
url = staticfiles_storage.url(rest)
......
from nose.tools import assert_equals
from static_replace import replace_static_urls, replace_course_urls
import re
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 xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore
......@@ -75,3 +78,34 @@ def test_data_dir_fallback(mock_storage, mock_modulestore, mock_settings):
mock_storage.exists.return_value = False
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.test import TestCase
import os
from override_settings import override_settings
from django.test.utils import override_settings
from tempfile import NamedTemporaryFile
from status import get_site_status_msg
......
......@@ -18,10 +18,13 @@ def jsdate_to_time(field):
"""
if field is None:
return field
elif isinstance(field, basestring): # iso format but ignores time zone assuming it's Z
d = datetime.datetime(*map(int, re.split('[^\d]', field)[:6])) # stop after seconds. Debatable
elif isinstance(field, basestring):
# 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()
elif isinstance(field, int) or isinstance(field, float):
elif isinstance(field, (int, long, float)):
return time.gmtime(field / 1000)
elif isinstance(field, time.struct_time):
return field
else:
raise ValueError("Couldn't convert %r to time" % field)
......@@ -632,8 +632,14 @@ class MultipleChoiceResponse(LoncapaResponse):
# define correct choices (after calling secondary setup)
xml = self.xml
cxml = xml.xpath('//*[@id=$id]//choice[@correct="true"]', id=xml.get('id'))
self.correct_choices = [contextualize_text(choice.get('name'), self.context) for choice in cxml]
cxml = xml.xpath('//*[@id=$id]//choice', id=xml.get('id'))
# 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):
'''
......@@ -999,7 +1005,7 @@ def sympy_check2():
self.context['debug'] = self.system.DEBUG
# exec the check function
if type(self.code) == str:
if isinstance(self.code, basestring):
try:
exec self.code in self.context['global_context'], self.context
correct = self.context['correct']
......
......@@ -50,6 +50,7 @@
},
smartIndent: false
});
$("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))});
});
</script>
</section>
<section id="designprotein2dinput_${id}" class="designprotein2dinput">
<div class="script_placeholder" data-src="/static/js/capa/protex/protex.nocache.js"/>
<div class="script_placeholder" data-src="/static/js/capa/protex/protex.nocache.js?raw"/>
<div class="script_placeholder" data-src="${applet_loader}"/>
% if status == 'unsubmitted':
......
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,8 +34,10 @@ setup(
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"timelimit = xmodule.timelimit_module:TimeLimitDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor",
"videoalpha = xmodule.videoalpha_module:VideoAlphaDescriptor",
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor",
......@@ -43,7 +45,8 @@ setup(
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor",
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor"
]
"graphical_slider_tool = xmodule.gst_module:GraphicalSliderToolDescriptor",
"foldit = xmodule.foldit_module:FolditDescriptor",
]
}
)
......@@ -446,7 +446,7 @@ class CourseDescriptor(SequenceDescriptor):
# utility function to get datetime objects for dates used to
# compute the is_new flag and the sorting_score
def to_datetime(timestamp):
return datetime.fromtimestamp(time.mktime(timestamp))
return datetime(*timestamp[:6])
def get_date(field):
timetuple = self._try_parse_time(field)
......@@ -648,7 +648,7 @@ class CourseDescriptor(SequenceDescriptor):
raise ValueError("First appointment date must be before last appointment date")
if self.registration_end_date > self.last_eligible_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):
"""
......@@ -704,6 +704,10 @@ class CourseDescriptor(SequenceDescriptor):
else:
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
def title(self):
return self.display_name
......
import logging
from lxml import etree
from dateutil import parser
from pkg_resources import resource_string
from xmodule.editing_module import EditingDescriptor
from xmodule.x_module import XModule
from xmodule.xml_module import XmlDescriptor
log = logging.getLogger(__name__)
class FolditModule(XModule):
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)
# ooh look--I'm lazy, so hardcoding the 7.00x required level.
# If we need it generalized, can pull from the xml later
self.required_level = 4
self.required_sublevel = 5
def parse_due_date():
"""
Pull out the date, or None
"""
s = self.metadata.get("due")
if s:
return parser.parse(s)
else:
return None
self.due_str = self.metadata.get("due", "None")
self.due = parse_due_date()
def is_complete(self):
"""
Did the user get to the required level before the due date?
"""
# We normally don't want django dependencies in xmodule. foldit is
# special. Import this late to avoid errors with things not yet being
# initialized.
from foldit.models import PuzzleComplete
complete = PuzzleComplete.is_level_complete(
self.system.anonymous_student_id,
self.required_level,
self.required_sublevel,
self.due)
return complete
def completed_puzzles(self):
"""
Return a list of puzzles that this user has completed, as an array of
dicts:
[ {'set': int,
'subset': int,
'created': datetime} ]
The list is sorted by set, then subset
"""
from foldit.models import PuzzleComplete
return sorted(
PuzzleComplete.completed_puzzles(self.system.anonymous_student_id),
key=lambda d: (d['set'], d['subset']))
def get_html(self):
"""
Render the html for the module.
"""
goal_level = '{0}-{1}'.format(
self.required_level,
self.required_sublevel)
context = {
'due': self.due_str,
'success': self.is_complete(),
'goal_level': goal_level,
'completed': self.completed_puzzles(),
}
return self.system.render_template('foldit.html', context)
def get_score(self):
"""
0 / 1 based on whether student has gotten far enough.
"""
score = 1 if self.is_complete() else 0
return {'score': score,
'total': self.max_score()}
def max_score(self):
return 1
class FolditDescriptor(XmlDescriptor, EditingDescriptor):
"""
Module for adding open ended response questions to courses
"""
mako_template = "widgets/html-edit.html"
module_class = FolditModule
filename_extension = "xml"
stores_state = True
has_score = True
template_dir_name = "foldit"
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor"
# The grade changes without any student interaction with the edx website,
# so always need to actually check.
always_recalculate_grades = True
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
For now, don't need anything from the xml
"""
return {}
......@@ -3,7 +3,11 @@ from xmodule.raw_module import RawDescriptor
class HiddenModule(XModule):
pass
def get_html(self):
if self.system.user_is_staff:
return "ERROR: This module is unknown--students will not see it at all"
else:
return ""
class HiddenDescriptor(RawDescriptor):
......
*.js
# Please do not ignore *.js files. Some xmodules are written in JS.
......@@ -107,12 +107,13 @@ class @HTMLEditingDescriptor
# 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.
visualEditor.startContent = visualEditor.getContent({format: "raw", no_events: 1});
visualEditor.isNotDirty = true
@focusVisualEditor(visualEditor)
@showingVisualEditor = true
focusVisualEditor: (visualEditor) =>
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?
@$mceToolbar = $(@element).find('table.mceToolbar')
......
class @VideoAlpha
constructor: (element) ->
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
@start = @el.data('start')
@end = @el.data('end')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions').toString() == "true"
@el = $("#video_#{@id}")
if @parseYoutubeId(@el.data("streams")) is true
@videoType = "youtube"
@fetchMetadata()
@parseSpeed()
else
@videoType = "html5"
@parseHtml5Sources @el.data('mp4-source'), @el.data('webm-source'), @el.data('ogg-source')
@speeds = ['0.75', '1.0', '1.25', '1.50']
sub = @el.data('sub')
if (typeof sub isnt "string") or (sub.length is 0)
sub = ""
@show_captions = false
@videos =
"0.75": sub
"1.0": sub
"1.25": sub
"1.5": sub
@setSpeed $.cookie('video_speed')
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
if @show_captions is true
@hide_captions = $.cookie('hide_captions') == 'true'
else
@hide_captions = true
$.cookie('hide_captions', @hide_captions, expires: 3650, path: '/')
@el.addClass 'closed'
if ((@videoType is "youtube") and (YT.Player)) or ((@videoType is "html5") and (HTML5Video.Player))
@embed()
else
if @videoType is "youtube"
window.onYouTubePlayerAPIReady = =>
@embed()
else if @videoType is "html5"
window.onHTML5PlayerAPIReady = =>
@embed()
youtubeId: (speed)->
@videos[speed || @speed]
parseYoutubeId: (videos)->
return false if (typeof videos isnt "string") or (videos.length is 0)
@videos = {}
$.each videos.split(/,/), (index, video) =>
speed = undefined
video = video.split(/:/)
speed = parseFloat(video[0]).toFixed(2).replace(/\.00$/, ".0")
@videos[speed] = video[1]
true
parseHtml5Sources: (mp4Source, webmSource, oggSource)->
@html5Sources =
mp4: null
webm: null
ogg: null
@html5Sources.mp4 = mp4Source if (typeof mp4Source is "string") and (mp4Source.length > 0)
@html5Sources.webm = webmSource if (typeof webmSource is "string") and (webmSource.length > 0)
@html5Sources.ogg = oggSource if (typeof oggSource is "string") and (oggSource.length > 0)
parseSpeed: ->
@speeds = ($.map @videos, (url, speed) -> speed).sort()
@setSpeed $.cookie('video_speed')
setSpeed: (newSpeed, updateCookie)->
if @speeds.indexOf(newSpeed) isnt -1
@speed = newSpeed
if updateCookie isnt false
$.cookie "video_speed", "" + newSpeed,
expires: 3650
path: "/"
else
@speed = "1.0"
embed: ->
@player = new VideoPlayerAlpha video: this
fetchMetadata: (url) ->
@metadata = {}
$.each @videos, (speed, url) =>
$.get "https://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp'
getDuration: ->
@metadata[@youtubeId()].duration
log: (eventName)->
logInfo =
id: @id
code: @youtubeId()
currentTime: @player.currentTime
speed: @speed
if @videoType is "youtube"
logInfo.code = @youtubeId()
else logInfo.code = "html5" if @videoType is "html5"
Logger.log eventName, logInfo
class @SubviewAlpha
constructor: (options) ->
$.each options, (key, value) =>
@[key] = value
@initialize()
@render()
@bind()
$: (selector) ->
$(selector, @el)
initialize: ->
render: ->
bind: ->
class @VideoCaptionAlpha extends SubviewAlpha
initialize: ->
@loaded = false
bind: ->
$(window).bind('resize', @resize)
@$('.hide-subtitles').click @toggle
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
.mousemove(@onMovement).bind('mousewheel', @onMovement)
.bind('DOMMouseScroll', @onMovement)
captionURL: ->
"#{@captionAssetPath}#{@youtubeId}.srt.sjson"
render: ->
# TODO: make it so you can have a video with no captions.
#@$('.video-wrapper').after """
# <ol class="subtitles"><li>Attempting to load captions...</li></ol>
# """
@$('.video-wrapper').after """
<ol class="subtitles"></ol>
"""
@$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
"""#"
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption()
fetchCaption: ->
$.getWithPrefix @captionURL(), (captions) =>
@captions = captions.text
@start = captions.start
@loaded = true
if onTouchBasedDevice()
$('.subtitles li').html "Caption will be displayed when you start playing the video."
else
@renderCaption()
renderCaption: ->
container = $('<ol>')
$.each @captions, (index, text) =>
container.append $('<li>').html(text).attr
'data-index': index
'data-start': @start[index]
@$('.subtitles').html(container.html())
@$('.subtitles li[data-index]').click @seekPlayer
# prepend and append an empty <li> for cosmetic reason
@$('.subtitles').prepend($('<li class="spacing">').height(@topSpacingHeight()))
.append($('<li class="spacing">').height(@bottomSpacingHeight()))
@rendered = true
search: (time) ->
if @loaded
min = 0
max = @start.length - 1
while min < max
index = Math.ceil((max + min) / 2)
if time < @start[index]
max = index - 1
if time >= @start[index]
min = index
return min
play: ->
if @loaded
@renderCaption() unless @rendered
@playing = true
pause: ->
if @loaded
@playing = false
updatePlayTime: (time) ->
if @loaded
# This 250ms offset is required to match the video speed
time = Math.round(Time.convert(time, @currentSpeed, '1.0') * 1000 + 250)
newIndex = @search time
if newIndex != undefined && @currentIndex != newIndex
if @currentIndex
@$(".subtitles li.current").removeClass('current')
@$(".subtitles li[data-index='#{newIndex}']").addClass('current')
@currentIndex = newIndex
@scrollCaption()
resize: =>
@$('.subtitles').css maxHeight: @captionHeight()
@$('.subtitles .spacing:first').height(@topSpacingHeight())
@$('.subtitles .spacing:last').height(@bottomSpacingHeight())
@scrollCaption()
onMouseEnter: =>
clearTimeout @frozen if @frozen
@frozen = setTimeout @onMouseLeave, 10000
onMovement: =>
@onMouseEnter()
onMouseLeave: =>
clearTimeout @frozen if @frozen
@frozen = null
@scrollCaption() if @playing
scrollCaption: ->
if !@frozen && @$('.subtitles .current:first').length
@$('.subtitles').scrollTo @$('.subtitles .current:first'),
offset: - @calculateOffset(@$('.subtitles .current:first'))
seekPlayer: (event) =>
event.preventDefault()
time = Math.round(Time.convert($(event.target).data('start'), '1.0', @currentSpeed) / 1000)
$(@).trigger('seek', time)
calculateOffset: (element) ->
@captionHeight() / 2 - element.height() / 2
topSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):first'))
bottomSpacingHeight: ->
@calculateOffset(@$('.subtitles li:not(.spacing):last'))
toggle: (event) =>
event.preventDefault()
if @el.hasClass('closed') # Captions are "closed" e.g. turned off
@hideCaptions(false)
else # Captions are on
@hideCaptions(true)
hideCaptions: (hide_captions) =>
if hide_captions
@$('.hide-subtitles').attr('title', 'Turn on captions')
@el.addClass('closed')
else
@$('.hide-subtitles').attr('title', 'Turn off captions')
@el.removeClass('closed')
@scrollCaption()
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: ->
if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height()
else
@$('.video-wrapper').height()
class @VideoControlAlpha extends SubviewAlpha
bind: ->
@$('.video_control').click @togglePlayback
render: ->
@el.append """
<div class="slider"></div>
<div>
<ul class="vcr">
<li><a class="video_control" href="#"></a></li>
<li>
<div class="vidtime">0:00 / 0:00</div>
</li>
</ul>
<div class="secondary-controls">
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div>
</div>
"""#"
unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play')
play: ->
@$('.video_control').removeClass('play').addClass('pause').html('Pause')
pause: ->
@$('.video_control').removeClass('pause').addClass('play').html('Play')
togglePlayback: (event) =>
event.preventDefault()
if @$('.video_control').hasClass('play')
$(@).trigger('play')
else if @$('.video_control').hasClass('pause')
$(@).trigger('pause')
class @VideoProgressSliderAlpha extends SubviewAlpha
initialize: ->
@buildSlider() unless onTouchBasedDevice()
buildSlider: ->
@slider = @el.slider
range: 'min'
change: @onChange
slide: @onSlide
stop: @onStop
@buildHandle()
buildHandle: ->
@handle = @$('.slider .ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
my: 'bottom center'
at: 'top center'
container: @handle
hide:
delay: 700
style:
classes: 'ui-tooltip-slider'
widget: true
play: =>
@buildSlider() unless @slider
updatePlayTime: (currentTime, duration) ->
if @slider && !@frozen
@slider.slider('option', 'max', duration)
@slider.slider('value', currentTime)
onSlide: (event, ui) =>
@frozen = true
@updateTooltip(ui.value)
$(@).trigger('seek', ui.value)
onChange: (event, ui) =>
@updateTooltip(ui.value)
onStop: (event, ui) =>
@frozen = true
$(@).trigger('seek', ui.value)
setTimeout (=> @frozen = false), 200
updateTooltip: (value)->
@handle.qtip('option', 'content.text', "#{Time.format(value)}")
class @VideoQualityControlAlpha extends SubviewAlpha
initialize: ->
@quality = null;
bind: ->
@$('.quality_control').click @toggleQuality
render: ->
@el.append """
<a href="#" class="quality_control" title="HD">HD</a>
"""#"
onQualityChange: (value) ->
@quality = value
if @quality in ['hd720', 'hd1080', 'highres']
@el.addClass('active')
else
@el.removeClass('active')
toggleQuality: (event) =>
event.preventDefault()
if @quality in ['hd720', 'hd1080', 'highres']
newQuality = 'large'
else
newQuality = 'hd720'
$(@).trigger('changeQuality', newQuality)
\ No newline at end of file
class @VideoSpeedControlAlpha extends SubviewAlpha
bind: ->
@$('.video_speeds a').click @changeVideoSpeed
if onTouchBasedDevice()
@$('.speeds').click (event) ->
event.preventDefault()
$(this).toggleClass('open')
else
@$('.speeds').mouseenter ->
$(this).addClass('open')
@$('.speeds').mouseleave ->
$(this).removeClass('open')
@$('.speeds').click (event) ->
event.preventDefault()
$(this).removeClass('open')
render: ->
@el.prepend """
<div class="speeds">
<a href="#">
<h3>Speed</h3>
<p class="active"></p>
</a>
<ol class="video_speeds"></ol>
</div>
"""
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
@$('.video_speeds').prepend($('<li>').attr('data-speed', speed).html(link))
@setSpeed @currentSpeed
reRender: (newSpeeds, currentSpeed) ->
@$('.video_speeds').empty()
@$('.video_speeds li').removeClass('active')
@speeds = newSpeeds
$.each @speeds, (index, speed) =>
link = $('<a>').attr(href: "#").html("#{speed}x")
listItem = $('<li>').attr('data-speed', speed).html(link);
listItem.addClass('active') if speed is currentSpeed
@$('.video_speeds').prepend listItem
@$('.video_speeds a').click @changeVideoSpeed
changeVideoSpeed: (event) =>
event.preventDefault()
unless $(event.target).parent().hasClass('active')
@currentSpeed = $(event.target).parent().data('speed')
$(@).trigger 'speedChange', $(event.target).parent().data('speed')
@setSpeed(parseFloat(@currentSpeed).toFixed(2).replace /\.00$/, '.0')
setSpeed: (speed) ->
@$('.video_speeds li').removeClass('active')
@$(".video_speeds li[data-speed='#{speed}']").addClass('active')
@$('.speeds p.active').html("#{speed}x")
class @VideoVolumeControlAlpha extends SubviewAlpha
initialize: ->
@currentVolume = 100
bind: ->
@$('.volume').mouseenter ->
$(this).addClass('open')
@$('.volume').mouseleave ->
$(this).removeClass('open')
@$('.volume>a').click(@toggleMute)
render: ->
@el.prepend """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""#"
@slider = @$('.volume-slider').slider
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @onChange
slide: @onChange
onChange: (event, ui) =>
@currentVolume = ui.value
$(@).trigger 'volumeChange', @currentVolume
@$('.volume').toggleClass 'muted', @currentVolume == 0
toggleMute: =>
if @currentVolume > 0
@previousVolume = @currentVolume
@slider.slider 'option', 'value', 0
else
@slider.slider 'option', 'value', @previousVolume
......@@ -73,7 +73,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
# 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.
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence')
need_uniq_names = ('problem', 'sequential', 'video', 'course', 'chapter', 'videosequence', 'timelimit')
attr = xml_data.attrib
tag = xml_data.tag
......
......@@ -2,6 +2,7 @@ import logging
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from fs.osfs import OSFS
from json import dumps
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir):
......@@ -27,6 +28,19 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
# export the course updates
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)
if 'grading_policy' in course.definition['data']:
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
grading_policy.write(dumps(course.definition['data']['grading_policy']))
# export all of the course metadata in policy.json
with course_run_policy_dir.open('policy.json', 'w') as course_policy:
policy = {}
policy = {'course/' + course.location.name: course.metadata}
course_policy.write(dumps(policy))
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)
......
......@@ -56,6 +56,10 @@ def update_templates():
available from the installed plugins
"""
# cdodge: build up a list of all existing templates. This will be used to determine which
# templates have been removed from disk - and thus we need to remove from the DB
templates_to_delete = modulestore('direct').get_items(['i4x', 'edx', 'templates', None, None, None])
for category, templates in all_templates().items():
for template in templates:
if 'display_name' not in template.metadata:
......@@ -85,3 +89,12 @@ def update_templates():
modulestore('direct').update_item(template_location, template.data)
modulestore('direct').update_children(template_location, template.children)
modulestore('direct').update_metadata(template_location, template.metadata)
# remove template from list of templates to delete
templates_to_delete = [t for t in templates_to_delete if t.location != template_location]
# now remove all templates which appear to have removed from disk
if len(templates_to_delete) > 0:
logging.debug('deleting dangling templates = {0}'.format(templates_to_delete))
for template in templates_to_delete:
modulestore('direct').delete_item(template.location)
---
metadata:
display_name: default
data_dir: a_made_up_name
data: |
<videoalpha youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>
children: []
......@@ -84,18 +84,21 @@ class ConditionalModuleTest(unittest.TestCase):
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
location = descriptor.location
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)
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):
return text
test_system.replace_urls = replace_urls
test_system.get_module = inner_get_module
module = inner_get_module(location)
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()
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_remaining_time_in_ms(self):
return int((self.ending_at - time()) * 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
......@@ -4,6 +4,8 @@ import logging
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from django.http import Http404
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.xml import XMLModuleStore
......@@ -13,9 +15,6 @@ from xmodule.contentstore.content import StaticContent
import datetime
import time
import datetime
import time
log = logging.getLogger(__name__)
......
import json
import logging
from lxml import etree
from pkg_resources import resource_string, resource_listdir
from django.http import Http404
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
import datetime
import time
log = logging.getLogger(__name__)
class VideoAlphaModule(XModule):
"""
XML source example:
<videoalpha show_captions="true"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
url_name="lecture_21_3" display_name="S19V3: Vacancies"
>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/>
</videoalpha>
"""
video_time = 0
icon_class = 'video'
js = {
'js': [resource_string(__name__, 'js/src/videoalpha/display/html5_video.js')],
'coffee':
[resource_string(__name__, 'js/src/time.coffee'),
resource_string(__name__, 'js/src/videoalpha/display.coffee')] +
[resource_string(__name__, 'js/src/videoalpha/display/' + filename)
for filename
in sorted(resource_listdir(__name__, 'js/src/videoalpha/display'))
if filename.endswith('.coffee')]}
css = {'scss': [resource_string(__name__, 'css/videoalpha/display.scss')]}
js_module_name = "VideoAlpha"
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)
xmltree = etree.fromstring(self.definition['data'])
self.youtube_streams = xmltree.get('youtube')
self.sub = xmltree.get('sub')
self.position = 0
self.show_captions = xmltree.get('show_captions', 'true')
self.sources = {
'main': self._get_source(xmltree),
'mp4': self._get_source(xmltree, ['mp4']),
'webm': self._get_source(xmltree, ['webm']),
'ogv': self._get_source(xmltree, ['ogv']),
}
self.track = self._get_track(xmltree)
self.start_time, self.end_time = self._get_timeframe(xmltree)
if instance_state is not None:
state = json.loads(instance_state)
if 'position' in state:
self.position = int(float(state['position']))
def _get_source(self, xmltree, exts=None):
"""Find the first valid source, which ends with one of `exts`."""
exts = ['mp4', 'ogv', 'avi', 'webm'] if exts is None else exts
condition = lambda src: any([src.endswith(ext) for ext in exts])
return self._get_first_external(xmltree, 'source', condition)
def _get_track(self, xmltree):
# find the first valid track
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag, condition=bool):
"""Will return the first 'valid' element of the given tag.
'valid' means that `condition('src' attribute) == True`
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src')
if condition(src):
result = src
break
return result
def _get_timeframe(self, xmltree):
""" Converts 'from' and 'to' parameters in video tag to seconds.
If there are no parameters, returns empty string. """
def parse_time(s):
"""Converts s in '12:34:45' format to seconds. If s is
None, returns empty string"""
if s is None:
return ''
else:
x = time.strptime(s, '%H:%M:%S')
return datetime.timedelta(hours=x.tm_hour,
minutes=x.tm_min,
seconds=x.tm_sec).total_seconds()
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
def handle_ajax(self, dispatch, get):
"""Handle ajax calls to this video.
TODO (vshnayder): This is not being called right now, so the
position is not being saved.
"""
log.debug(u"GET {0}".format(get))
log.debug(u"DISPATCH {0}".format(dispatch))
if dispatch == 'goto_position':
self.position = int(float(get['position']))
log.info(u"NEW POSITION {0}".format(self.position))
return json.dumps({'success': True})
raise Http404()
def get_instance_state(self):
return json.dumps({'position': self.position})
def get_html(self):
if isinstance(modulestore(), MongoModuleStore):
caption_asset_path = StaticContent.get_base_url_path_for_course_assets(self.location) + '/subs_'
else:
# VS[compat]
# cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
return self.system.render_template('videoalpha.html', {
'youtube_streams': self.youtube_streams,
'id': self.location.html_id(),
'sub': self.sub,
'sources': self.sources,
'track': self.track,
'display_name': self.display_name,
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
'data_dir': self.metadata['data_dir'],
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions,
'start': self.start_time,
'end': self.end_time
})
class VideoAlphaDescriptor(RawDescriptor):
module_class = VideoAlphaModule
stores_state = True
template_dir_name = "videoalpha"
......@@ -515,6 +515,16 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self._child_instances = None
self._inherited_metadata = set()
# Class level variable
always_recalculate_grades = False
"""
Return whether this descriptor always requires recalculation of grades, for
example if the score can change via an extrnal service, not just when the
student interacts with the module on the page. A specific example is
FoldIt, which posts grade-changing updates through a separate API.
"""
@property
def display_name(self):
'''
......
......@@ -50,7 +50,7 @@ if Backbone?
convertMath: ->
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]]
renderResponses: ->
......
......@@ -47,7 +47,7 @@ if Backbone?
convertMath: ->
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]]
toggleVote: (event) ->
......
......@@ -26,7 +26,7 @@ if Backbone?
convertMath: ->
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]]
markAsStaff: ->
......
......@@ -30,7 +30,7 @@ if Backbone?
convertMath: ->
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]]
markAsStaff: ->
......
......@@ -22,6 +22,15 @@
// It calls protexIsReady with a deferred command when it has finished
// initialization and has drawn itself
function updateProtexField() {
var problem = $('#protex_container').parents('.problem');
var input_field = problem.find('input[type=hidden]');
var protex_answer = protexCheckAnswer();
var value = {protex_answer: protex_answer};
//console.log(JSON.stringify(value));
input_field.val(JSON.stringify(value));
}
protexIsReady = function() {
//Load target shape
var target_shape = $('#target_shape').val();
......@@ -29,15 +38,18 @@
//Get answer from protex and store it into the hidden input field
//when Check button is clicked
var problem = $('#protex_container').parents('.problem');
var check_button = problem.find('input.check');
var input_field = problem.find('input[type=hidden]');
check_button.on('click', function() {
var fold_button = $("#fold-button");
fold_button.on('click', function(){
var problem = $('#protex_container').parents('.problem');
var input_field = problem.find('input[type=hidden]');
var protex_answer = protexCheckAnswer();
var value = {protex_answer: protex_answer};
//console.log(JSON.stringify(value));
input_field.val(JSON.stringify(value));
});
});
updateProtexField();
};
/*function initializeProtex() {
//Check to see if the two exported GWT functions protexSetTargetShape
......
from django.core.urlresolvers import reverse
from override_settings import override_settings
from django.test.utils import override_settings
import xmodule.modulestore.django
......
......@@ -343,6 +343,14 @@ def get_score(course_id, user, problem_descriptor, module_creator, student_modul
Can return None if user doesn't have access, or if something else went wrong.
cache: A StudentModuleCache
"""
if problem_descriptor.always_recalculate_grades:
problem = module_creator(problem_descriptor)
d = problem.get_score()
if d is not None:
return (d['score'], d['total'])
else:
return (None, None)
if not (problem_descriptor.stores_state and problem_descriptor.has_score):
# These are not problems, and do not have a score
return (None, None)
......
......@@ -13,14 +13,8 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types
"""
from django.db import models
#from django.core.cache import cache
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):
"""
Keeps student state for a particular module in a particular course.
......@@ -30,6 +24,7 @@ class StudentModule(models.Model):
MODULE_TYPES = (('problem', 'problem'),
('video', 'video'),
('html', 'html'),
('timelimit', 'timelimit'),
)
## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
......
......@@ -33,7 +33,7 @@ from xmodule_modifiers import replace_course_urls, replace_static_urls, add_hist
from xmodule.modulestore.exceptions import ItemNotFoundError
from statsd import statsd
log = logging.getLogger("mitx.courseware")
log = logging.getLogger(__name__)
if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
......@@ -306,6 +306,7 @@ def _get_module(user, request, descriptor, student_module_cache, course_id,
# Make an error module
return err_descriptor.xmodule_constructor(system)(None, None)
system.set('user_is_staff', has_access(user, descriptor.location, 'staff', course_id))
_get_html = module.get_html
if wrap_xmodule_display == True:
......
......@@ -11,7 +11,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
from django.core.urlresolvers import reverse
from override_settings import override_settings
from django.test.utils import override_settings
import xmodule.modulestore.django
from xmodule.modulestore.mongo import MongoModuleStore
......
......@@ -8,7 +8,7 @@ from django.core.context_processors import csrf
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
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 mitxmako.shortcuts import render_to_response, render_to_string
#from django.views.decorators.csrf import ensure_csrf_cookie
......@@ -20,7 +20,7 @@ from courseware.access import has_access
from courseware.courses import (get_courses, get_course_with_access,
get_courses_by_university, sort_by_announcement)
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 django_comment_client.utils import get_discussion_title
......@@ -153,6 +153,75 @@ def save_child_position(seq_module, child_name, instance_module):
instance_module.state = seq_module.get_instance_state()
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 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
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_duration' 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 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:
if 'suppress_toplevel_navigation' in timelimit_descriptor.metadata:
context['suppress_toplevel_navigation'] = timelimit_descriptor.metadata['suppress_toplevel_navigation']
return context
@login_required
@ensure_csrf_cookie
......@@ -222,7 +291,7 @@ def index(request, course_id, chapter=None, section=None,
instance_module = get_instance_module(course_id, request.user, course_module, student_module_cache)
save_child_position(course_module, chapter, instance_module)
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)
if chapter_module is None:
......@@ -251,7 +320,20 @@ def index(request, course_id, chapter=None, section=None,
instance_module = get_instance_module(course_id, request.user, chapter_module, student_module_cache)
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_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()
else:
# section is none, so display a message
......
......@@ -6,7 +6,7 @@ from django.conf import settings
from mock import Mock
from override_settings import override_settings
from django.test.utils import override_settings
import xmodule.modulestore.django
......
......@@ -11,6 +11,8 @@ from django.http import HttpResponse
from django.utils import simplejson
from django_comment_client.models import Role
from django_comment_client.permissions import check_permissions_by_view
from xmodule.modulestore.exceptions import NoPathToItem
from mitxmako import middleware
import pystache_custom as pystache
......@@ -164,6 +166,7 @@ def initialize_discussion_info(course):
# 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)
path_to_locations = {}
for module in all_modules:
skip_module = False
for key in ('id', 'discussion_category', 'for'):
......@@ -171,6 +174,14 @@ def initialize_discussion_info(course):
log.warning("Required key '%s' not in discussion %s, leaving out of category map" % (key, module.location))
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:
continue
......@@ -235,6 +246,7 @@ def initialize_discussion_info(course):
_DISCUSSIONINFO[course.id]['id_map'] = discussion_id_map
_DISCUSSIONINFO[course.id]['category_map'] = category_map
_DISCUSSIONINFO[course.id]['timestamp'] = datetime.now()
_DISCUSSIONINFO[course.id]['path_to_location'] = path_to_locations
class JsonResponse(HttpResponse):
......@@ -390,11 +402,23 @@ def get_courseware_context(content, course):
if id in id_map:
location = id_map[id]["location"].url()
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,
"chapter": chapter,
"section": section,
"position": position})
# cdodge: did we pre-compute, if so, then let's use that rather than recomputing
if 'path_to_location' in _DISCUSSIONINFO[course.id] and location in _DISCUSSIONINFO[course.id]['path_to_location']:
(course_id, chapter, section, position) = _DISCUSSIONINFO[course.id]['path_to_location'][location]
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}
return content_info
......
import logging
from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from student.models import unique_id_for_user
log = logging.getLogger(__name__)
class Score(models.Model):
"""
This model stores the scores of different users on FoldIt problems.
"""
user = models.ForeignKey(User, db_index=True,
related_name='foldit_scores')
# The XModule that wants to access this doesn't have access to the real
# userid. Save the anonymized version so we can look up by that.
unique_user_id = models.CharField(max_length=50, db_index=True)
puzzle_id = models.IntegerField()
best_score = models.FloatField(db_index=True)
current_score = models.FloatField(db_index=True)
score_version = models.IntegerField()
created = models.DateTimeField(auto_now_add=True)
class PuzzleComplete(models.Model):
"""
This keeps track of the sets of puzzles completed by each user.
e.g. PuzzleID 1234, set 1, subset 3. (Sets and subsets correspond to levels
in the intro puzzles)
"""
class Meta:
# there should only be one puzzle complete entry for any particular
# puzzle for any user
unique_together = ('user', 'puzzle_id', 'puzzle_set', 'puzzle_subset')
ordering = ['puzzle_id']
user = models.ForeignKey(User, db_index=True,
related_name='foldit_puzzles_complete')
# The XModule that wants to access this doesn't have access to the real
# userid. Save the anonymized version so we can look up by that.
unique_user_id = models.CharField(max_length=50, db_index=True)
puzzle_id = models.IntegerField()
puzzle_set = models.IntegerField(db_index=True)
puzzle_subset = models.IntegerField(db_index=True)
created = models.DateTimeField(auto_now_add=True)
def __unicode__(self):
return "PuzzleComplete({0}, id={1}, set={2}, subset={3}, created={4})".format(
self.user.username, self.puzzle_id,
self.puzzle_set, self.puzzle_subset,
self.created)
@staticmethod
def completed_puzzles(anonymous_user_id):
"""
Return a list of puzzles that this user has completed, as an array of
dicts:
[ {'set': int,
'subset': int,
'created': datetime} ]
"""
complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id)
return [{'set': c.puzzle_set,
'subset': c.puzzle_subset,
'created': c.created} for c in complete]
@staticmethod
def is_level_complete(anonymous_user_id, level, sub_level, due=None):
"""
Return True if this user completed level--sub_level by due.
Users see levels as e.g. 4-5.
Args:
level: int
sub_level: int
due (optional): If specified, a datetime. Ignored if None.
"""
complete = PuzzleComplete.objects.filter(unique_user_id=anonymous_user_id,
puzzle_set=level,
puzzle_subset=sub_level)
if due is not None:
complete = complete.filter(created__lte=due)
return complete.exists()
import json
import logging
from functools import partial
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.client import RequestFactory
from django.conf import settings
from django.core.urlresolvers import reverse
from foldit.views import foldit_ops, verify_code
from foldit.models import PuzzleComplete
from student.models import UserProfile, unique_id_for_user
from datetime import datetime, timedelta
log = logging.getLogger(__name__)
class FolditTestCase(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.url = reverse('foldit_ops')
pwd = 'abc'
self.user = User.objects.create_user('testuser', 'test@test.com', pwd)
self.unique_user_id = unique_id_for_user(self.user)
now = datetime.now()
self.tomorrow = now + timedelta(days=1)
self.yesterday = now - timedelta(days=1)
UserProfile.objects.create(user=self.user)
def make_request(self, post_data):
request = self.factory.post(self.url, post_data)
request.user = self.user
return request
def test_SetPlayerPuzzleScores(self):
scores = [ {"PuzzleID": 994391,
"ScoreType": "score",
"BestScore": 0.078034,
"CurrentScore":0.080035,
"ScoreVersion":23}]
scores_str = json.dumps(scores)
verify = {"Verify": verify_code(self.user.email, scores_str),
"VerifyMethod":"FoldItVerify"}
data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
'SetPlayerPuzzleScores': scores_str}
request = self.make_request(data)
response = foldit_ops(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, json.dumps(
[{"OperationID": "SetPlayerPuzzleScores",
"Value": [{
"PuzzleID": 994391,
"Status": "Success"}]}]))
def test_SetPlayerPuzzleScores_many(self):
scores = [ {"PuzzleID": 994391,
"ScoreType": "score",
"BestScore": 0.078034,
"CurrentScore":0.080035,
"ScoreVersion":23},
{"PuzzleID": 994392,
"ScoreType": "score",
"BestScore": 0.078000,
"CurrentScore":0.080011,
"ScoreVersion":23}]
scores_str = json.dumps(scores)
verify = {"Verify": verify_code(self.user.email, scores_str),
"VerifyMethod":"FoldItVerify"}
data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
'SetPlayerPuzzleScores': scores_str}
request = self.make_request(data)
response = foldit_ops(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, json.dumps(
[{"OperationID": "SetPlayerPuzzleScores",
"Value": [{
"PuzzleID": 994391,
"Status": "Success"},
{"PuzzleID": 994392,
"Status": "Success"}]}]))
def test_SetPlayerPuzzleScores_error(self):
scores = [ {"PuzzleID": 994391,
"ScoreType": "score",
"BestScore": 0.078034,
"CurrentScore":0.080035,
"ScoreVersion":23}]
validation_str = json.dumps(scores)
verify = {"Verify": verify_code(self.user.email, validation_str),
"VerifyMethod":"FoldItVerify"}
# change the real string -- should get an error
scores[0]['ScoreVersion'] = 22
scores_str = json.dumps(scores)
data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
'SetPlayerPuzzleScores': scores_str}
request = self.make_request(data)
response = foldit_ops(request)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response.content,
json.dumps([{
"OperationID": "SetPlayerPuzzleScores",
"Success": "false",
"ErrorString": "Verification failed",
"ErrorCode": "VerifyFailed"}]))
def make_puzzles_complete_request(self, puzzles):
"""
Make a puzzles complete request, given an array of
puzzles. E.g.
[ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
"""
puzzles_str = json.dumps(puzzles)
verify = {"Verify": verify_code(self.user.email, puzzles_str),
"VerifyMethod":"FoldItVerify"}
data = {'SetPuzzlesCompleteVerify': json.dumps(verify),
'SetPuzzlesComplete': puzzles_str}
request = self.make_request(data)
response = foldit_ops(request)
self.assertEqual(response.status_code, 200)
return response
@staticmethod
def set_puzzle_complete_response(values):
return json.dumps([{"OperationID":"SetPuzzlesComplete",
"Value": values}])
def test_SetPlayerPuzzlesComplete(self):
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
response = self.make_puzzles_complete_request(puzzles)
self.assertEqual(response.content,
self.set_puzzle_complete_response([13, 53524]))
def test_SetPlayerPuzzlesComplete_multiple(self):
"""Check that state is stored properly"""
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
response = self.make_puzzles_complete_request(puzzles)
self.assertEqual(response.content,
self.set_puzzle_complete_response([13, 53524]))
puzzles = [ {"PuzzleID": 14, "Set": 1, "SubSet": 3},
{"PuzzleID": 15, "Set": 1, "SubSet": 1} ]
response = self.make_puzzles_complete_request(puzzles)
self.assertEqual(response.content,
self.set_puzzle_complete_response([13, 14, 15, 53524]))
def test_SetPlayerPuzzlesComplete_level_complete(self):
"""Check that the level complete function works"""
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
response = self.make_puzzles_complete_request(puzzles)
self.assertEqual(response.content,
self.set_puzzle_complete_response([13, 53524]))
puzzles = [ {"PuzzleID": 14, "Set": 1, "SubSet": 3},
{"PuzzleID": 15, "Set": 1, "SubSet": 1} ]
response = self.make_puzzles_complete_request(puzzles)
self.assertEqual(response.content,
self.set_puzzle_complete_response([13, 14, 15, 53524]))
is_complete = partial(
PuzzleComplete.is_level_complete, self.unique_user_id)
self.assertTrue(is_complete(1, 1))
self.assertTrue(is_complete(1, 3))
self.assertTrue(is_complete(1, 2))
self.assertFalse(is_complete(4, 5))
puzzles = [ {"PuzzleID": 74, "Set": 4, "SubSet": 5} ]
response = self.make_puzzles_complete_request(puzzles)
self.assertTrue(is_complete(4, 5))
# Now check due dates
self.assertTrue(is_complete(1, 1, due=self.tomorrow))
self.assertFalse(is_complete(1, 1, due=self.yesterday))
def test_SetPlayerPuzzlesComplete_error(self):
puzzles = [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
{"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
puzzles_str = json.dumps(puzzles)
verify = {"Verify": verify_code(self.user.email, puzzles_str + "x"),
"VerifyMethod":"FoldItVerify"}
data = {'SetPuzzlesCompleteVerify': json.dumps(verify),
'SetPuzzlesComplete': puzzles_str}
request = self.make_request(data)
response = foldit_ops(request)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(response.content,
json.dumps([{
"OperationID": "SetPuzzlesComplete",
"Success": "false",
"ErrorString": "Verification failed",
"ErrorCode": "VerifyFailed"}]))
import hashlib
import json
import logging
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from foldit.models import Score, PuzzleComplete
from student.models import unique_id_for_user
log = logging.getLogger(__name__)
@login_required
@csrf_exempt
@require_POST
def foldit_ops(request):
"""
Endpoint view for foldit operations.
"""
responses = []
if "SetPlayerPuzzleScores" in request.POST:
puzzle_scores_json = request.POST.get("SetPlayerPuzzleScores")
pz_verify_json = request.POST.get("SetPlayerPuzzleScoresVerify")
log.debug("SetPlayerPuzzleScores message: puzzle scores: %r",
puzzle_scores_json)
puzzle_score_verify = json.loads(pz_verify_json)
if not verifies_ok(request.user.email,
puzzle_scores_json, puzzle_score_verify):
responses.append({"OperationID": "SetPlayerPuzzleScores",
"Success": "false",
"ErrorString": "Verification failed",
"ErrorCode": "VerifyFailed"})
log.warning("Verification of SetPlayerPuzzleScores failed:" +
"user %s, scores json %r, verify %r",
request.user, puzzle_scores_json, pz_verify_json)
else:
puzzle_scores = json.loads(puzzle_scores_json)
responses.append(save_scores(request.user, puzzle_scores))
if "SetPuzzlesComplete" in request.POST:
puzzles_complete_json = request.POST.get("SetPuzzlesComplete")
pc_verify_json = request.POST.get("SetPuzzlesCompleteVerify")
log.debug("SetPuzzlesComplete message: %r",
puzzles_complete_json)
puzzles_complete_verify = json.loads(pc_verify_json)
if not verifies_ok(request.user.email,
puzzles_complete_json, puzzles_complete_verify):
responses.append({"OperationID": "SetPuzzlesComplete",
"Success": "false",
"ErrorString": "Verification failed",
"ErrorCode": "VerifyFailed"})
log.warning("Verification of SetPuzzlesComplete failed:" +
" user %s, puzzles json %r, verify %r",
request.user, puzzles_complete_json, pc_verify_json)
else:
puzzles_complete = json.loads(puzzles_complete_json)
responses.append(save_complete(request.user, puzzles_complete))
return HttpResponse(json.dumps(responses))
def verify_code(email, val):
"""
Given the email and passed in value (str), return the expected
verification code.
"""
# TODO: is this the right string?
verification_string = email.lower() + '|' + val
return hashlib.md5(verification_string).hexdigest()
def verifies_ok(email, val, verification):
"""
Check that the hash_str matches the expected hash of val.
Returns True if verification ok, False otherwise
"""
if verification.get("VerifyMethod") != "FoldItVerify":
log.debug("VerificationMethod in %r isn't FoldItVerify", verification)
return False
hash_str = verification.get("Verify")
return verify_code(email, val) == hash_str
def save_scores(user, puzzle_scores):
score_responses = []
for score in puzzle_scores:
log.debug("score: %s", score)
# expected keys ScoreType, PuzzleID (int),
# BestScore (energy), CurrentScore (Energy), ScoreVersion (int)
puzzle_id = score['PuzzleID']
# TODO: save the score
# SetPlayerPuzzleScoreResponse object
score_responses.append({'PuzzleID': puzzle_id,
'Status': 'Success'})
return {"OperationID": "SetPlayerPuzzleScores", "Value": score_responses}
def save_complete(user, puzzles_complete):
"""
Returned list of PuzzleIDs should be in sorted order (I don't think client
cares, but tests do)
"""
for complete in puzzles_complete:
log.debug("Puzzle complete: %s", complete)
puzzle_id = complete['PuzzleID']
puzzle_set = complete['Set']
puzzle_subset = complete['SubSet']
# create if not there
PuzzleComplete.objects.get_or_create(
user=user,
unique_user_id=unique_id_for_user(user),
puzzle_id=puzzle_id,
puzzle_set=puzzle_set,
puzzle_subset=puzzle_subset)
# List of all puzzle ids of intro-level puzzles completed ever, including on this
# request
# TODO: this is just in this request...
complete_responses = list(pc.puzzle_id
for pc in PuzzleComplete.objects.filter(user=user))
return {"OperationID": "SetPuzzlesComplete", "Value": complete_responses}
......@@ -15,7 +15,7 @@ import json
from nose import SkipTest
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
from django.contrib.auth.models import Group
......
......@@ -386,6 +386,46 @@ def instructor_dashboard(request, course_id):
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
elif 'List beta testers' in action:
......
......@@ -23,7 +23,7 @@ from mitxmako.shortcuts import render_to_string
import logging
log = logging.getLogger(__name__)
from override_settings import override_settings
from django.test.utils import override_settings
from django.http import QueryDict
from xmodule.tests import test_util_open_ended
......
......@@ -53,7 +53,6 @@ with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
SITE_NAME = ENV_TOKENS['SITE_NAME']
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
CSRF_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
BOOK_URL = ENV_TOKENS['BOOK_URL']
MEDIA_URL = ENV_TOKENS['MEDIA_URL']
......@@ -110,4 +109,4 @@ PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
PEARSON = AUTH_TOKENS.get("PEARSON")
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
\ No newline at end of file
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
......@@ -200,7 +200,6 @@ COURSE_TITLE = "Circuits and Electronics"
### Dark code. Should be enabled in local settings for devel.
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = False
WIKI_ENABLED = False
......@@ -569,6 +568,9 @@ INSTALLED_APPS = (
'wiki.plugins.notifications',
'course_wiki.plugins.markdownedx',
# foldit integration
'foldit',
# For testing
'django.contrib.admin', # only used in DEBUG mode
......
......@@ -34,7 +34,6 @@ EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
DEBUG = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = True
MAKO_TEMPLATES['course'] = [DATA_DIR, EDX4EDX_ROOT]
......
......@@ -6,7 +6,6 @@ COURSE_TITLE = "edx4edx: edX Author Course"
EDX4EDX_ROOT = ENV_ROOT / "data/edx4edx"
### Dark code. Should be enabled in local settings for devel.
QUICKEDIT = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
###
PIPELINE_CSS_COMPRESSOR = None
......
This is a library for edx4edx, allowing users to practice writing problems.
#!/usr/bin/python
from random import choice
import string
import traceback
from django.conf import settings
import capa.capa_problem as lcp
from dogfood.views import update_problem
def GenID(length=8, chars=string.letters + string.digits):
return ''.join([choice(chars) for i in range(length)])
randomid = GenID()
def check_problem_code(ans, the_lcp, correct_answers, false_answers):
"""
ans = student's answer
the_lcp = LoncapaProblem instance
returns dict {'ok':is_ok,'msg': message with iframe}
"""
pfn = "dog%s" % randomid
pfn += the_lcp.problem_id.replace('filename', '') # add problem ID to dogfood problem name
update_problem(pfn, ans, filestore=the_lcp.system.filestore)
msg = '<hr width="100%"/>'
msg += '<iframe src="%s/dogfood/filename%s" width="95%%" height="400" frameborder="1">No iframe support!</iframe>' % (settings.MITX_ROOT_URL, pfn)
msg += '<hr width="100%"/>'
endmsg = """<p><font size="-1" color="purple">Note: if the code text box disappears after clicking on "Check",
please type something in the box to make it refresh properly. This is a
bug with Chrome; it does not happen with Firefox. It is being fixed.
</font></p>"""
is_ok = True
if (not correct_answers) or (not false_answers):
ret = {'ok': is_ok,
'msg': msg + endmsg,
}
return ret
try:
# check correctness
fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn)
test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system)
if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1') == 'correct'):
is_ok = False
if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1') == 'correct'):
is_ok = False
except Exception, err:
is_ok = False
msg += "<p>Error: %s</p>" % str(err).replace('<', '&#60;')
msg += "<p><pre>%s</pre></p>" % traceback.format_exc().replace('<', '&#60;')
ret = {'ok': is_ok,
'msg': msg + endmsg,
}
return ret
......@@ -238,8 +238,7 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
###### PMathML input ######
# convert mathml answer to formula
try:
if dynamath:
mmlans = dynamath[0]
mmlans = dynamath[0] if dynamath else None
except Exception, err:
mmlans = None
if not mmlans:
......
......@@ -22,7 +22,7 @@
@import 'course/courseware/sidebar';
@import 'course/courseware/amplifier';
@import 'course/layout/calculator';
@import 'course/layout/timer';
// 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
......
......@@ -248,3 +248,17 @@ section.self-assessment {
font-weight: bold;
}
}
section.foldit {
table {
margin-top: 10px;
}
th {
text-align: center;
}
td {
padding-left: 5px;
padding-right: 5px;
}
}
\ No newline at end of file
......@@ -67,7 +67,7 @@ section.course-index {
}
.chapter {
width: 100%;
width: 100% !important;
@include box-sizing(border-box);
padding: 11px 14px;
@include linear-gradient(top, rgba(255, 255, 255, .6), rgba(255, 255, 255, 0));
......@@ -99,6 +99,8 @@ section.course-index {
@include border-radius(0);
margin: 0;
padding: 9px 0 9px 9px;
overflow: auto;
width: 100%;
li {
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;
}
}
......@@ -101,7 +101,7 @@ header.global {
margin-right: 5px;
> a {
@include background-image(linear-gradient(-90deg, #fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%));
@include background-image(linear-gradient(#fff 0%, rgb(250,250,250) 50%, rgb(237,237,237) 50%, rgb(220,220,220) 100%));
border: 1px solid transparent;
border-color: rgb(200,200,200);
@include border-radius(3px);
......
......@@ -32,7 +32,7 @@
<!-- TODO: http://docs.jquery.com/Plugins/Validation -->
<script type="text/javascript">
document.write('\x3Cscript type="text/javascript" src="' +
document.location.protocol + '//www.youtube.com/player_api">\x3C/script>');
document.location.protocol + '//www.youtube.com/iframe_api">\x3C/script>');
</script>
<script type="text/javascript">
......@@ -59,12 +59,76 @@
});
});
</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>
<%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">
<div class="course-wrapper">
% if accordion:
<section aria-label="Course Navigation" class="course-index">
<header id="open_close_accordion">
<a href="#">close</a>
......@@ -76,6 +140,7 @@
</nav>
</div>
</section>
% endif
<section class="course-content">
${content}
......
......@@ -64,6 +64,7 @@ function goto( mode)
<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('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> ]
</h2>
......@@ -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 instructor_access:
<hr width="40%" style="align:left">
......
<%namespace name='static' file='static_content.html'/>
<!DOCTYPE html>
<html>
## -----------------------------------------------------------------------------
## Template for lib.dogfood.views.dj_capa_problem
##
## Used for viewing assesment problems in "dogfood" self-evaluation mode
## -----------------------------------------------------------------------------
<head>
<link rel="stylesheet" href="${static.url('css/vendor/jquery.treeview.css')}" type="text/css" media="all" />
## <link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" />
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
## <%static:css group='application'/>
% endif
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
% endif
<script type="text/javascript" src="${static.url('js/jquery.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery-ui.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script>
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
<%static:js group='application'/>
% endif
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
% for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]:
<script type="text/javascript" src="${jsfn}"></script>
% endfor
% endif
## codemirror
<link rel="stylesheet" href="/static/css/codemirror.css" type="text/css" media="all" />
<script type="text/javascript" src="${ settings.LIB_URL }codemirror-compressed.js"></script>
## alternate codemirror
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/lib/codemirror.js"></script>
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/xml/xml.js"></script>
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/python/python.js"></script>
## image input: for clicking on images (see imageinput.html)
<script type="text/javascript" src="/static/js/imageinput.js"></script>
<%include file="mathjax_include.html" />
</head>
<body class="courseware">
<!--[if lt IE 9]>
<script src="/static/js/html5shiv.js"></script>
<![endif]-->
<div class="courseware"></div>
## -----------------------------------------------------------------------------
## information
## <hr width="100%">
## <h2>Rendition of your problem code</h2>
## <hr width="100%">
## -----------------------------------------------------------------------------
## rendered problem display
<script>
${init_js}
</script>
<style type="text/css">
.problem-header {display:none;}
.staff {display:none;}
.correct { display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
background: url("/static/images/correct-icon.png") center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px; }
.incorrect{
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
background: url("/static/images/incorrect-icon.png") center center no-repeat;
height: 20px;
width: 20px;
position: relative;
top: 6px; }
.unanswered {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
background: url("/static/images/unanswered-icon.png") center center no-repeat;
height: 14px;
position: relative;
top: 4px;
width: 14px; }
}
</style>
<meta name="path_prefix" content="${MITX_ROOT_URL}">
<section class="course-content">
<form>
${phtml}
</form>
</section>
<script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.qtip.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.cookie.js')}"></script>
## <script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
## image input: for clicking on images (see imageinput.html)
<script type="text/javascript" src="/static/js/imageinput.js"></script>
<script type="text/javascript" >
var codemirror_set= {}; // associative array of codemirror objects
</script>
<%block name="js_extra"/>
</body>
</html>
<section class="foldit">
<p><strong>Due:</strong> ${due}
<p>
<strong>Status:</strong>
% if success:
You have successfully gotten to level ${goal_level}.
% else:
You have not yet gotten to level ${goal_level}.
% endif
</p>
<h3>Completed puzzles</h3>
<table>
<tr>
<th>Level</th>
<th>Submitted</th>
</tr>
% for puzzle in completed:
<tr>
<td>${'{0}-{1}'.format(puzzle['set'], puzzle['subset'])}</td>
<td>${puzzle['created'].strftime('%Y-%m-%d %H:%M')}</td>
</tr>
% endfor
</table>
</section>
\ No newline at end of file
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>edX gitupdate</title>
</head>
<body>
<hr>
<h1>edX gitupdate</h1>
<hr>
<h2>Coursename: ${coursename}</h2>
% if msg:
${msg}
% else:
<p>
Do you REALLY want to overwrite all the course.xml + problems + html
files with version from the main git repository?
</p>
<form method="post">
<input type="submit" value="Do git update" name="gitupdate" />
## <input type="submit" value="Cancel" name="cancel" />
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf}"/>
</form>
% endif
<p><a href="${MITX_ROOT_URL}/courseware/">Return to site</a></p>
</body> </html>
......@@ -29,13 +29,18 @@
<body class="<%block name='bodyclass'/>">
% if not suppress_toplevel_navigation:
<%include file="navigation.html" />
% endif
<section class="content-wrapper">
${self.body()}
<%block name="bodyextra"/>
</section>
% if not suppress_toplevel_navigation:
<%include file="footer.html" />
% endif
<%static:js group='application'/>
<%static:js group='module-js'/>
......
<%namespace name='static' file='static_content.html'/>
<!DOCTYPE html>
<html>
## -----------------------------------------------------------------------------
## Template for courseware.views.quickedit
##
## Used for quick-edit link present when viewing capa-format assesment problems.
## -----------------------------------------------------------------------------
<head>
<link rel="stylesheet" href="${static.url('css/vendor/jquery.treeview.css')}" type="text/css" media="all" />
## <link rel="stylesheet" href="${ settings.LIB_URL }jquery.treeview.css" type="text/css" media="all" />
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
<%static:css group='application'/>
% endif
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
## <link rel="stylesheet" href="/static/sass/application.css" type="text/css" media="all" / >
% endif
<script type="text/javascript" src="${static.url('js/jquery.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery-ui.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/swfobject/swfobject.js')}"></script>
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
<%static:js group='application'/>
% endif
% if not settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
% for jsfn in [ '/static/%s' % x.replace('.coffee','.js') for x in settings.PIPELINE_JS['application']['source_filenames'] ]:
<script type="text/javascript" src="${jsfn}"></script>
% endfor
% endif
## codemirror
<link rel="stylesheet" href="/static/css/codemirror.css" type="text/css" media="all" />
<script type="text/javascript" src="${ settings.LIB_URL }codemirror-compressed.js"></script>
## alternate codemirror
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/lib/codemirror.js"></script>
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/xml/xml.js"></script>
## <script type="text/javascript" src="/static/js/CodeMirror-2.25/mode/python/python.js"></script>
## image input: for clicking on images (see imageinput.html)
<script type="text/javascript" src="/static/js/imageinput.js"></script>
## <script type="text/javascript">
## var codemirror_set = {}; // track all codemirror textareas, so they can be refreshed on page changes
## </script>
<!--[if lt IE 9]>
<script src="${static.url('js/html5shiv.js')}"></script>
<![endif]-->
<%block name="headextra"/>
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
MathJax extension libraries -->
<%include file="mathjax_include.html" />
</head>
<body class="courseware" style="text-align:left;" >
<style type="text/css">
.CodeMirror {border-style: solid;
border-width: 1px;}
.CodeMirror-scroll {
height: 500;
width: 100%
}
</style>
## -----------------------------------------------------------------------------
## information and i4x PSL code
<hr width="100%">
<h2>QuickEdit</h2>
<hr width="100%">
<ul>
<li>File = ${filename}</li>
<li>ID = ${id}</li>
</ul>
<form method="post">
<textarea rows="40" cols="160" name="quickedit_${id}" id="quickedit_${id}">${pxmls|h}</textarea>
<br/>
<input type="submit" value="Change Problem" name="qesubmit" />
<input type="submit" value="Revert to original" name="qesubmit" />
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf}"/>
</form>
<span>${msg|n}</span>
## -----------------------------------------------------------------------------
## rendered problem display
<script>
// height: auto;
// overflow-y: hidden;
// overflow-x: auto;
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("quickedit_${id}"),
{ 'mode': {name: "xml", alignCDATA: true},
lineNumbers: true
});
// $('.my-wymeditor').wymeditor();
});
</script>
<hr width="100%">
<script>
${init_js}
</script>
<style type="text/css">
.staff {display:none;}
.correct { display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
background: url("/static/images/correct-icon.png") center center no-repeat;
height: 20px;
position: relative;
top: 6px;
width: 25px; }
.incorrect{
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: baseline;
zoom: 1;
*display: inline;
*vertical-align: auto;
background: url("/static/images/incorrect-icon.png") center center no-repeat;
height: 20px;
width: 20px;
position: relative;
top: 6px; }
}
</style>
<meta name="path_prefix" content="${MITX_ROOT_URL}">
<section class="course-content">
<div id="seq_content">
<form>
${phtml}
</form>
</div>
</section>
<script type="text/javascript" src="${static.url('js/jquery.treeview.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.leanModal.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.qtip.min.js')}"></script>
<script type="text/javascript" src="${static.url('js/jquery.cookie.js')}"></script>
## <script type="text/javascript" src="${static.url('js/video_player.js')}"></script>
<script type="text/javascript" src="${static.url('js/schematic.js')}"></script>
<script type="text/javascript" src="${static.url('js/cktsim.js')}"></script>
<script type="text/javascript" >
var codemirror_set= {}; // associative array of codemirror objects
</script>
<script type="text/javascript" src="${static.url('js/jquery.scrollTo-1.4.2-min.js')}"></script>
<%block name="js_extra"/>
</body>
</html>
${module_content}
%if edit_link:
%if location.category in ['problem','video','html']:
% if edit_link:
<div>
<a href="${edit_link}">Edit</a> /
<a href="#${element_id}_xqa-modal" onclick="javascript:getlog('${element_id}', {
......@@ -9,7 +10,7 @@ ${module_content}
'user': '${user}'
})" id="${element_id}_xqa_log">QA</a>
</div>
% endif
% endif
<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" >
......@@ -74,3 +75,5 @@ category = ${category | h}
'user': '${user}'
});
</script>
%endif
% if display_name is not UNDEFINED and display_name is not None:
<h2> ${display_name} </h2>
% endif
%if settings.MITX_FEATURES['STUB_VIDEO_FOR_TESTING']:
<div id="stub_out_video_for_testing"></div>
%else:
<div
id="video_${id}"
class="video"
data-streams="${youtube_streams}"
${'data-sub="{}"'.format(sub) if sub else ''}
${'data-mp4-source="{}"'.format(sources.get('mp4')) if sources.get('mp4') else ''}
${'data-webm-source="{}"'.format(sources.get('webm')) if sources.get('webm') else ''}
${'data-ogg-source="{}"'.format(sources.get('ogv')) if sources.get('ogv') else ''}
data-caption-data-dir="${data_dir}"
data-show-captions="${show_captions}"
data-start="${start}"
data-end="${end}"
data-caption-asset-path="${caption_asset_path}"
>
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
<div id="${id}"></div>
</section>
<section class="video-controls"></section>
</article>
</div>
</div>
%endif
% if sources.get('main'):
<div class="video-sources">
<p>Download video <a href="${sources.get('main')}">here</a>.</p>
</div>
% endif
% if track:
<div class="video-tracks">
<p>Download subtitles <a href="${track}">here</a>.</p>
</div>
% endif
......@@ -320,10 +320,6 @@ if settings.COURSEWARE_ENABLED:
'courseware.views.static_tab', name="static_tab"),
)
if settings.QUICKEDIT:
urlpatterns += (url(r'^quickedit/(?P<id>[^/]*)$', 'dogfood.views.quickedit'),)
urlpatterns += (url(r'^dogfood/(?P<id>[^/]*)$', 'dogfood.views.df_capa_problem'),)
if settings.ENABLE_JASMINE:
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
......@@ -361,6 +357,12 @@ if settings.MITX_FEATURES.get('ENABLE_SQL_TRACKING_LOGS'):
url(r'^event_logs/(?P<args>.+)$', 'track.views.view_tracking_log'),
)
# FoldIt views
urlpatterns += (
# The path is hardcoded into their app...
url(r'^comm/foldit_ops', 'foldit.views.foldit_ops', name="foldit_ops"),
)
urlpatterns = patterns(*urlpatterns)
if settings.DEBUG:
......
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