Commit 55c69a1e by Chris Dodge

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into…

Merge branch 'feature/cale/cms-master' of github.com:MITx/mitx into feature/cdodge/import-course-info

Conflicts:
	lms/djangoapps/courseware/module_render.py
parents b788b9d6 e35d1483
Instructions
============
For each pull request, add one or more lines to the bottom of the change list. When
code is released to production, change the `Upcoming` entry to todays date, and add
a new block at the bottom of the file.
Upcoming
--------
Change log entries should be targeted at end users. A good place to start is the
user story that instigated the pull request.
Changes
=======
Upcoming
--------
* Created changelog
\ No newline at end of file
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User, Group
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--list',
action='store_true',
dest='list',
default=False,
help='List available groups'),
make_option('--create',
action='store_true',
dest='create',
default=False,
help='Create the group if it does not exist'),
make_option('--remove',
action='store_true',
dest='remove',
default=False,
help='Remove the user from the group instead of adding it'),
)
args = '<user|email> <group>'
help = 'Add a user to a group'
def print_groups(self):
print 'Groups available:'
for group in Group.objects.all().distinct():
print ' ', group.name
def handle(self, *args, **options):
if options['list']:
self.print_groups()
return
if len(args) != 2:
raise CommandError('Usage is add_to_group {0}'.format(self.args))
name_or_email, group_name = args
if '@' in name_or_email:
user = User.objects.get(email=name_or_email)
else:
user = User.objects.get(username=name_or_email)
try:
group = Group.objects.get(name=group_name)
except Group.DoesNotExist:
if options['create']:
group = Group(name=group_name)
group.save()
else:
raise CommandError('Group {} does not exist'.format(group_name))
if options['remove']:
user.groups.remove(group)
else:
user.groups.add(group)
print 'Success!'
from optparse import make_option
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
import re import re
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + (
args = '<user/email user/email ...>' make_option('--unset',
action='store_true',
dest='unset',
default=False,
help='Set is_staff to False instead of True'),
)
args = '<user|email> [user|email ...]>'
help = """ help = """
This command will set isstaff to true for one or more users. This command will set is_staff to true for one or more users.
Lookup by username or email address, assumes usernames Lookup by username or email address, assumes usernames
do not look like email addresses. do not look like email addresses.
""" """
def handle(self, *args, **kwargs): def handle(self, *args, **options):
if len(args) < 1: if len(args) < 1:
print Command.help raise CommandError('Usage is set_staff {0}'.format(self.args))
return
for user in args: for user in args:
if re.match('[^@]+@[^@]+\.[^@]+', user): if re.match('[^@]+@[^@]+\.[^@]+', user):
try: try:
v = User.objects.get(email=user) v = User.objects.get(email=user)
except: except:
raise CommandError("User {0} does not exist".format( raise CommandError("User {0} does not exist".format(user))
user))
else: else:
try: try:
v = User.objects.get(username=user) v = User.objects.get(username=user)
except: except:
raise CommandError("User {0} does not exist".format( raise CommandError("User {0} does not exist".format(user))
user))
if options['unset']:
v.is_staff = False
else:
v.is_staff = True
v.is_staff = True
v.save() v.save()
print 'Success!'
...@@ -39,6 +39,8 @@ from collections import namedtuple ...@@ -39,6 +39,8 @@ from collections import namedtuple
from courseware.courses import get_courses_by_university from courseware.courses import get_courses_by_university
from courseware.access import has_access from courseware.access import has_access
from statsd import statsd
log = logging.getLogger("mitx.student") log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
...@@ -204,7 +206,13 @@ def change_enrollment(request): ...@@ -204,7 +206,13 @@ def change_enrollment(request):
return {'success': False, return {'success': False,
'error': 'enrollment in {} not allowed at this time' 'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)} .format(course.display_name)}
org, course_num, run=course_id.split("/")
statsd.increment("common.student.enrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)])
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
return {'success': True} return {'success': True}
...@@ -212,6 +220,13 @@ def change_enrollment(request): ...@@ -212,6 +220,13 @@ def change_enrollment(request):
try: try:
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id) enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
enrollment.delete() enrollment.delete()
org, course_num, run=course_id.split("/")
statsd.increment("common.student.unenrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)])
return {'success': True} return {'success': True}
except CourseEnrollment.DoesNotExist: except CourseEnrollment.DoesNotExist:
return {'success': False, 'error': 'You are not enrolled for this course.'} return {'success': False, 'error': 'You are not enrolled for this course.'}
...@@ -260,7 +275,9 @@ def login_user(request, error=""): ...@@ -260,7 +275,9 @@ def login_user(request, error=""):
log.info("Login success - {0} ({1})".format(username, email)) log.info("Login success - {0} ({1})".format(username, email))
try_change_enrollment(request) try_change_enrollment(request)
statsd.increment("common.student.successful_login")
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
log.warning("Login failed - Account not active for user {0}, resending activation".format(username)) log.warning("Login failed - Account not active for user {0}, resending activation".format(username))
...@@ -466,7 +483,9 @@ def create_account(request, post_override=None): ...@@ -466,7 +483,9 @@ def create_account(request, post_override=None):
log.debug('bypassing activation email') log.debug('bypassing activation email')
login_user.is_active = True login_user.is_active = True
login_user.save() login_user.save()
statsd.increment("common.student.account_created")
js = {'success': True} js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
......
...@@ -32,10 +32,13 @@ from xml.sax.saxutils import unescape ...@@ -32,10 +32,13 @@ from xml.sax.saxutils import unescape
import chem import chem
import chem.chemcalc import chem.chemcalc
import chem.chemtools
import calc import calc
from correctmap import CorrectMap from correctmap import CorrectMap
import eia import eia
import inputtypes import inputtypes
import customrender
from util import contextualize_text, convert_files_to_filenames from util import contextualize_text, convert_files_to_filenames
import xqueue_interface import xqueue_interface
...@@ -45,22 +48,8 @@ import responsetypes ...@@ -45,22 +48,8 @@ import responsetypes
# dict of tagname, Response Class -- this should come from auto-registering # dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__]) response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
# Different ways students can input code
entry_types = ['textline',
'schematic',
'textbox',
'imageinput',
'optioninput',
'choicegroup',
'radiogroup',
'checkboxgroup',
'filesubmission',
'javascriptinput',
'crystallography',
'chemicalequationinput',]
# extra things displayed after "show answers" is pressed # extra things displayed after "show answers" is pressed
solution_types = ['solution'] solution_tags = ['solution']
# these get captured as student responses # these get captured as student responses
response_properties = ["codeparam", "responseparam", "answer"] response_properties = ["codeparam", "responseparam", "answer"]
...@@ -77,7 +66,8 @@ global_context = {'random': random, ...@@ -77,7 +66,8 @@ global_context = {'random': random,
'scipy': scipy, 'scipy': scipy,
'calc': calc, 'calc': calc,
'eia': eia, 'eia': eia,
'chemcalc': chem.chemcalc} 'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools}
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"] html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
...@@ -305,7 +295,7 @@ class LoncapaProblem(object): ...@@ -305,7 +295,7 @@ class LoncapaProblem(object):
answer_map.update(results) answer_map.update(results)
# include solutions from <solution>...</solution> stanzas # include solutions from <solution>...</solution> stanzas
for entry in self.tree.xpath("//" + "|//".join(solution_types)): for entry in self.tree.xpath("//" + "|//".join(solution_tags)):
answer = etree.tostring(entry) answer = etree.tostring(entry)
if answer: if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context) answer_map[entry.get('id')] = contextualize_text(answer, self.context)
...@@ -483,7 +473,7 @@ class LoncapaProblem(object): ...@@ -483,7 +473,7 @@ class LoncapaProblem(object):
problemid = problemtree.get('id') # my ID problemid = problemtree.get('id') # my ID
if problemtree.tag in inputtypes.registered_input_tags(): if problemtree.tag in inputtypes.registry.registered_tags():
# If this is an inputtype subtree, let it render itself. # If this is an inputtype subtree, let it render itself.
status = "unsubmitted" status = "unsubmitted"
msg = '' msg = ''
...@@ -509,7 +499,7 @@ class LoncapaProblem(object): ...@@ -509,7 +499,7 @@ class LoncapaProblem(object):
'hint': hint, 'hint': hint,
'hintmode': hintmode,}} 'hintmode': hintmode,}}
input_type_cls = inputtypes.get_class_for_tag(problemtree.tag) input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
the_input = input_type_cls(self.system, problemtree, state) the_input = input_type_cls(self.system, problemtree, state)
return the_input.get_html() return the_input.get_html()
...@@ -517,9 +507,15 @@ class LoncapaProblem(object): ...@@ -517,9 +507,15 @@ class LoncapaProblem(object):
if problemtree in self.responders: if problemtree in self.responders:
return self.responders[problemtree].render_html(self._extract_html) return self.responders[problemtree].render_html(self._extract_html)
# let each custom renderer render itself:
if problemtree.tag in customrender.registry.registered_tags():
renderer_class = customrender.registry.get_class_for_tag(problemtree.tag)
renderer = renderer_class(self.system, problemtree)
return renderer.get_html()
# otherwise, render children recursively, and copy over attributes
tree = etree.Element(problemtree.tag) tree = etree.Element(problemtree.tag)
for item in problemtree: for item in problemtree:
# render child recursively
item_xhtml = self._extract_html(item) item_xhtml = self._extract_html(item)
if item_xhtml is not None: if item_xhtml is not None:
tree.append(item_xhtml) tree.append(item_xhtml)
...@@ -556,11 +552,12 @@ class LoncapaProblem(object): ...@@ -556,11 +552,12 @@ class LoncapaProblem(object):
response_id += 1 response_id += 1
answer_id = 1 answer_id = 1
input_tags = inputtypes.registry.registered_tags()
inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x
for x in (entry_types + solution_types)]), for x in (input_tags + solution_tags)]),
id=response_id_str) id=response_id_str)
# assign one answer_id for each entry_type or solution_type # assign one answer_id for each input type or solution type
for entry in inputfields: for entry in inputfields:
entry.attrib['response_id'] = str(response_id) entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id) entry.attrib['answer_id'] = str(answer_id)
......
"""This module originally includes functions for grading Vsepr problems.
Also, may be this module is the place for other chemistry-related grade functions. TODO: discuss it.
"""
import json
import unittest
import itertools
def vsepr_parse_user_answer(user_input):
"""
user_input is json generated by vsepr.js from dictionary.
There are must be only two keys in original user_input dictionary: "geometry" and "atoms".
Format: u'{"geometry": "AX3E0","atoms":{"c0": "B","p0": "F","p1": "B","p2": "F"}}'
Order of elements inside "atoms" subdict does not matters.
Return dict from parsed json.
"Atoms" subdict stores positions of atoms in molecule.
General types of positions:
c0 - central atom
p0..pN - peripheral atoms
a0..aN - axial atoms
e0..eN - equatorial atoms
Each position is dictionary key, i.e. user_input["atoms"]["c0"] is central atom, user_input["atoms"]["a0"] is one of axial atoms.
Special position only for AX6 (Octahedral) geometry:
e10, e12 - atom pairs opposite the central atom,
e20, e22 - atom pairs opposite the central atom,
e1 and e2 pairs lying crosswise in equatorial plane.
In user_input["atoms"] may be only 3 set of keys:
(c0,p0..pN),
(c0, a0..aN, e0..eN),
(c0, a0, a1, e10,e11,e20,e21) - if geometry is AX6.
"""
return json.loads(user_input)
def vsepr_build_correct_answer(geometry, atoms):
"""
geometry is string.
atoms is dict of atoms with proper positions.
Example:
correct_answer = vsepr_build_correct_answer(geometry="AX4E0", atoms={"c0": "N", "p0": "H", "p1": "(ep)", "p2": "H", "p3": "H"})
returns a dictionary composed from input values:
{'geometry': geometry, 'atoms': atoms}
"""
return {'geometry': geometry, 'atoms': atoms}
def vsepr_grade(user_input, correct_answer, convert_to_peripheral=False):
"""
This function does comparison between user_input and correct_answer.
Comparison is successful if all steps are successful:
1) geometries are equal
2) central atoms (index in dictionary 'c0') are equal
3):
In next steps there is comparing of corresponding subsets of atom positions: equatorial (e0..eN), axial (a0..aN) or peripheral (p0..pN)
If convert_to_peripheral is True, then axial and equatorial positions are converted to peripheral.
This means that user_input from:
"atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "(ep)","e20": "H","e21": "(ep)"}}' after parsing to json
is converted to:
{"c0": "Br", "p0": "(ep)", "p1": "test", "p2": "H", "p3": "H", "p4": "(ep)", "p6": "(ep)"}
i.e. aX and eX -> pX
So if converted, p subsets are compared,
if not a and e subsets are compared
If all subsets are equal, grade succeeds.
There is also one special case for AX6 geometry.
In this case user_input["atoms"] contains special 3 symbol keys: e10, e12, e20, and e21.
Correct answer for this geometry can be of 3 types:
1) c0 and peripheral
2) c0 and axial and equatorial
3) c0 and axial and equatorial-subset-1 (e1X) and equatorial-subset-2 (e2X)
If correct answer is type 1 or 2, then user_input is converted from type 3 to type 2 (or to type 1 if convert_to_peripheral is True)
If correct_answer is type 3, then we done special case comparison. We have 3 sets of atoms positions both in user_input and correct_answer: axial, eq-1 and eq-2.
Answer will be correct if these sets are equals for one of permutations. For example, if :
user_axial = correct_eq-1
user_eq-1 = correct-axial
user_eq-2 = correct-eq-2
"""
if user_input['geometry'] != correct_answer['geometry']:
return False
if user_input['atoms']['c0'] != correct_answer['atoms']['c0']:
return False
if convert_to_peripheral:
# convert user_input from (a,e,e1,e2) to (p)
# correct_answer must be set in (p) using this flag
c0 = user_input['atoms'].pop('c0')
user_input['atoms'] = {'p' + str(i): v for i, v in enumerate(user_input['atoms'].values())}
user_input['atoms']['c0'] = c0
# special case for AX6
if 'e10' in correct_answer['atoms']: # need check e1x, e2x symmetry for AX6..
a_user = {}
a_correct = {}
for ea_position in ['a', 'e1', 'e2']: # collecting positions:
a_user[ea_position] = [v for k, v in user_input['atoms'].items() if k.startswith(ea_position)]
a_correct[ea_position] = [v for k, v in correct_answer['atoms'].items() if k.startswith(ea_position)]
correct = [sorted(a_correct['a'])] + [sorted(a_correct['e1'])] + [sorted(a_correct['e2'])]
for permutation in itertools.permutations(['a', 'e1', 'e2']):
if correct == [sorted(a_user[permutation[0]])] + [sorted(a_user[permutation[1]])] + [sorted(a_user[permutation[2]])]:
return True
return False
else: # no need to check e1x,e2x symmetry - convert them to ex
if 'e10' in user_input['atoms']: # e1x exists, it is AX6.. case
e_index = 0
for k, v in user_input['atoms'].items():
if len(k) == 3: # e1x
del user_input['atoms'][k]
user_input['atoms']['e' + str(e_index)] = v
e_index += 1
# common case
for ea_position in ['p', 'a', 'e']:
# collecting atoms:
a_user = [v for k, v in user_input['atoms'].items() if k.startswith(ea_position)]
a_correct = [v for k, v in correct_answer['atoms'].items() if k.startswith(ea_position)]
# print a_user, a_correct
if len(a_user) != len(a_correct):
return False
if sorted(a_user) != sorted(a_correct):
return False
return True
class Test_Grade(unittest.TestCase):
''' test grade function '''
def test_incorrect_geometry(self):
correct_answer = vsepr_build_correct_answer(geometry="AX4E0", atoms={"c0": "N", "p0": "H", "p1": "(ep)", "p2": "H", "p3": "H"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX3E0","atoms":{"c0": "B","p0": "F","p1": "B","p2": "F"}}')
self.assertFalse(vsepr_grade(user_answer, correct_answer))
def test_correct_answer_p(self):
correct_answer = vsepr_build_correct_answer(geometry="AX4E0", atoms={"c0": "N", "p0": "H", "p1": "(ep)", "p2": "H", "p3": "H"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX4E0","atoms":{"c0": "N","p0": "H","p1": "(ep)","p2": "H", "p3": "H"}}')
self.assertTrue(vsepr_grade(user_answer, correct_answer))
def test_correct_answer_ae(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "test", "a1": "(ep)", "e0": "H", "e1": "H", "e2": "(ep)", "e3": "(ep)"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "H","e20": "(ep)","e21": "(ep)"}}')
self.assertTrue(vsepr_grade(user_answer, correct_answer))
def test_correct_answer_ae_convert_to_p_but_input_not_in_p(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "test", "e0": "H", "e1": "H", "e2": "(ep)", "e3": "(ep)"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "(ep)","e20": "H","e21": "(ep)"}}')
self.assertFalse(vsepr_grade(user_answer, correct_answer, convert_to_peripheral=True))
def test_correct_answer_ae_convert_to_p(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "p0": "(ep)", "p1": "test", "p2": "H", "p3": "H", "p4": "(ep)", "p6": "(ep)"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "(ep)","e20": "H","e21": "(ep)"}}')
self.assertTrue(vsepr_grade(user_answer, correct_answer, convert_to_peripheral=True))
def test_correct_answer_e1e2_in_a(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "(ep)","a1": "(ep)","e10": "H","e11": "H","e20": "H","e21": "H"}}')
self.assertTrue(vsepr_grade(user_answer, correct_answer))
def test_correct_answer_e1e2_in_e1(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "H","a1": "H","e10": "(ep)","e11": "(ep)","e20": "H","e21": "H"}}')
self.assertTrue(vsepr_grade(user_answer, correct_answer))
def test_correct_answer_e1e2_in_e2(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "H","a1": "H","e10": "H","e11": "H","e20": "(ep)","e21": "(ep)"}}')
self.assertTrue(vsepr_grade(user_answer, correct_answer))
def test_incorrect_answer_e1e2(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "H","a1": "H","e10": "(ep)","e11": "H","e20": "H","e21": "(ep)"}}')
self.assertFalse(vsepr_grade(user_answer, correct_answer))
def test_incorrect_c0(self):
correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "test", "e0": "H", "e1": "H", "e2": "H", "e3": "(ep)"})
user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "H","a0": "test","a1": "(ep)","e0": "H","e1": "H","e2": "(ep)","e3": "H"}}')
self.assertFalse(vsepr_grade(user_answer, correct_answer))
def suite():
testcases = [Test_Grade]
suites = []
for testcase in testcases:
suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase))
return unittest.TestSuite(suites)
if __name__ == "__main__":
unittest.TextTestRunner(verbosity=2).run(suite())
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
# #
# Used by responsetypes and capa_problem # Used by responsetypes and capa_problem
class CorrectMap(object): class CorrectMap(object):
""" """
Stores map between answer_id and response evaluation result for each question Stores map between answer_id and response evaluation result for each question
...@@ -69,7 +68,7 @@ class CorrectMap(object): ...@@ -69,7 +68,7 @@ class CorrectMap(object):
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
means that when the definition of CorrectMap (e.g. its properties) are altered, means that when the definition of CorrectMap (e.g. its properties) are altered,
an existing correct_map dict not coincide with the newest CorrectMap format as an existing correct_map dict will not coincide with the newest CorrectMap format as
defined by self.set. defined by self.set.
For graceful migration, feed the contents of each correct map to self.set, rather than For graceful migration, feed the contents of each correct map to self.set, rather than
......
"""
This has custom renderers: classes that know how to render certain problem tags (e.g. <math> and
<solution>) to html.
These tags do not have state, so they just get passed the system (for access to render_template),
and the xml element.
"""
from registry import TagRegistry
import logging
import re
import shlex # for splitting quoted strings
import json
from lxml import etree
import xml.sax.saxutils as saxutils
from registry import TagRegistry
log = logging.getLogger('mitx.' + __name__)
registry = TagRegistry()
#-----------------------------------------------------------------------------
class MathRenderer(object):
tags = ['math']
def __init__(self, system, xml):
'''
Render math using latex-like formatting.
Examples:
<math>$\displaystyle U(r)=4 U_0 $</math>
<math>$r_0$</math>
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
TODO: use shorter tags (but this will require converting problem XML files!)
'''
self.system = system
self.xml = xml
mathstr = re.sub('\$(.*)\$', r'[mathjaxinline]\1[/mathjaxinline]', xml.text)
mtag = 'mathjax'
if not r'\displaystyle' in mathstr:
mtag += 'inline'
else:
mathstr = mathstr.replace(r'\displaystyle', '')
self.mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag)
def get_html(self):
"""
Return the contents of this tag, rendered to html, as an etree element.
"""
# TODO: why are there nested html tags here?? Why are there html tags at all, in fact?
html = '<html><html>%s</html><html>%s</html></html>' % (
self.mathstr, saxutils.escape(self.xml.tail))
try:
xhtml = etree.XML(html)
except Exception as err:
if self.system.DEBUG:
msg = '<html><div class="inline-error"><p>Error %s</p>' % (
str(err).replace('<', '&lt;'))
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
html.replace('<', '&lt;'))
msg += "</div></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
return xhtml
registry.register(MathRenderer)
#-----------------------------------------------------------------------------
class SolutionRenderer(object):
'''
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
extended answer (a problem "solution") after "show answers" is pressed.
Note that the solution content is NOT rendered and returned in the HTML. It is obtained by an
ajax call.
'''
tags = ['solution']
def __init__(self, system, xml):
self.system = system
self.id = xml.get('id')
def get_html(self):
context = {'id': self.id}
html = self.system.render_template("solutionspan.html", context)
return etree.XML(html)
registry.register(SolutionRenderer)
...@@ -6,11 +6,9 @@ ...@@ -6,11 +6,9 @@
Module containing the problem elements which render into input objects Module containing the problem elements which render into input objects
- textline - textline
- textbox (change this to textarea?) - textbox (aka codeinput)
- schemmatic - schematic
- choicegroup - choicegroup (aka radiogroup, checkboxgroup)
- radiogroup
- checkboxgroup
- javascriptinput - javascriptinput
- imageinput (for clickable image) - imageinput (for clickable image)
- optioninput (for option list) - optioninput (for option list)
...@@ -23,64 +21,34 @@ Each input type takes the xml tree as 'element', the previous answer as 'value', ...@@ -23,64 +21,34 @@ Each input type takes the xml tree as 'element', the previous answer as 'value',
graded status as'status' graded status as'status'
""" """
# TODO: rename "state" to "status" for all below. status is currently the answer for the # TODO: there is a lot of repetitive "grab these elements from xml attributes, with these defaults,
# problem ID for the input element, but it will turn into a dict containing both the # put them in the context" code. Refactor so class just specifies required and optional attrs (with
# answer and any associated message for the problem ID for the input element. # defaults for latter), and InputTypeBase does the right thing.
# TODO: Quoting and unquoting is handled in a pretty ad-hoc way. Also something that could be done
# properly once in InputTypeBase.
# Possible todo: make inline the default for textlines and other "one-line" inputs. It probably
# makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a
# general css and layout strategy for capa, document it, then implement it.
import json
import logging import logging
from lxml import etree
import re import re
import shlex # for splitting quoted strings import shlex # for splitting quoted strings
import json import sys
from lxml import etree
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
from registry import TagRegistry
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
######################################################################### #########################################################################
_TAGS_TO_CLASSES = {} registry = TagRegistry()
def register_input_class(cls):
"""
Register cls as a supported input type. It is expected to have the same constructor as
InputTypeBase, and to define cls.tags as a list of tags that it implements.
If an already-registered input type has claimed one of those tags, will raise ValueError.
If there are no tags in cls.tags, will also raise ValueError.
"""
# Do all checks and complain before changing any state.
if len(cls.tags) == 0:
raise ValueError("No supported tags for class {0}".format(cls.__name__))
for t in cls.tags:
if t in _TAGS_TO_CLASSES:
other_cls = _TAGS_TO_CLASSES[t]
if cls == other_cls:
# registering the same class multiple times seems silly, but ok
continue
raise ValueError("Tag {0} already registered by class {1}. Can't register for class {2}"
.format(t, other_cls.__name__, cls.__name__))
# Ok, should be good to change state now.
for t in cls.tags:
_TAGS_TO_CLASSES[t] = cls
def registered_input_tags():
"""
Get a list of all the xml tags that map to known input types.
"""
return _TAGS_TO_CLASSES.keys()
def get_class_for_tag(tag):
"""
For any tag in registered_input_tags(), return the corresponding class. Otherwise, will raise KeyError.
"""
return _TAGS_TO_CLASSES[tag]
class InputTypeBase(object): class InputTypeBase(object):
""" """
...@@ -93,16 +61,18 @@ class InputTypeBase(object): ...@@ -93,16 +61,18 @@ class InputTypeBase(object):
""" """
Instantiate an InputType class. Arguments: Instantiate an InputType class. Arguments:
- system : ModuleSystem instance which provides OS, rendering, and user context. Specifically, must - system : ModuleSystem instance which provides OS, rendering, and user context.
have a render_template function. Specifically, must have a render_template function.
- xml : Element tree of this Input element - xml : Element tree of this Input element
- state : a dictionary with optional keys: - state : a dictionary with optional keys:
* 'value' * 'value' -- the current value of this input
* 'id' (what the student entered last time)
* 'id' -- the id of this input, typically
"{problem-location}_{response-num}_{input-num}"
* 'status' (answered, unanswered, unsubmitted) * 'status' (answered, unanswered, unsubmitted)
* 'feedback' (dictionary containing keys for hints, errors, or other * 'feedback' (dictionary containing keys for hints, errors, or other
feedback from previous attempt. Specifically 'message', 'hint', 'hintmode'. If 'hintmode' feedback from previous attempt. Specifically 'message', 'hint',
is 'always', the hint is always displayed.) 'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
""" """
self.xml = xml self.xml = xml
...@@ -132,6 +102,26 @@ class InputTypeBase(object): ...@@ -132,6 +102,26 @@ class InputTypeBase(object):
self.status = state.get('status', 'unanswered') self.status = state.get('status', 'unanswered')
# Call subclass "constructor" -- means they don't have to worry about calling
# super().__init__, and are isolated from changes to the input constructor interface.
try:
self.setup()
except Exception as err:
# Something went wrong: add xml to message, but keep the traceback
msg = "Error in xml '{x}': {err} ".format(x=etree.tostring(xml), err=str(err))
raise Exception, msg, sys.exc_info()[2]
def setup(self):
"""
InputTypes should override this to do any needed initialization. It is called after the
constructor, so all base attributes will be set.
If this method raises an exception, it will be wrapped with a message that includes the
problem xml.
"""
pass
def _get_render_context(self): def _get_render_context(self):
""" """
Abstract method. Subclasses should implement to return the dictionary Abstract method. Subclasses should implement to return the dictionary
...@@ -146,40 +136,13 @@ class InputTypeBase(object): ...@@ -146,40 +136,13 @@ class InputTypeBase(object):
Return the html for this input, as an etree element. Return the html for this input, as an etree element.
""" """
if self.template is None: if self.template is None:
raise NotImplementedError("no rendering template specified for class {0}".format(self.__class__)) raise NotImplementedError("no rendering template specified for class {0}"
.format(self.__class__))
html = self.system.render_template(self.template, self._get_render_context()) html = self.system.render_template(self.template, self._get_render_context())
return etree.XML(html) return etree.XML(html)
## TODO: Remove once refactor is complete
def make_class_for_render_function(fn):
"""
Take an old-style render function, return a new-style input class.
"""
class Impl(InputTypeBase):
"""
Inherit all the constructor logic from InputTypeBase...
"""
tags = [fn.__name__]
def get_html(self):
"""...delegate to the render function to do the work"""
return fn(self.xml, self.value, self.status, self.system.render_template, self.msg)
# don't want all the classes to be called Impl (confuses register_input_class).
Impl.__name__ = fn.__name__.capitalize()
return Impl
def _reg(fn):
"""
Register an old-style inputtype render function as a new-style subclass of InputTypeBase.
This will go away once converting all input types to the new format is complete. (TODO)
"""
register_input_class(make_class_for_render_function(fn))
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -195,100 +158,98 @@ class OptionInput(InputTypeBase): ...@@ -195,100 +158,98 @@ class OptionInput(InputTypeBase):
template = "optioninput.html" template = "optioninput.html"
tags = ['optioninput'] tags = ['optioninput']
def _get_render_context(self): def setup(self):
return _optioninput(self.xml, self.value, self.status, self.system.render_template, self.msg) # Extract the options...
options = self.xml.get('options')
if not options:
raise ValueError("optioninput: Missing 'options' specification.")
# parse the set of possible options
oset = shlex.shlex(options[1:-1])
oset.quotes = "'"
oset.whitespace = ","
oset = [x[1:-1] for x in list(oset)]
def optioninput(element, value, status, render_template, msg=''): # make ordered list with (key, value) same
context = _optioninput(element, value, status, render_template, msg) self.osetdict = [(oset[x], oset[x]) for x in range(len(oset))]
html = render_template("optioninput.html", context) # TODO: allow ordering to be randomized
return etree.XML(html)
def _optioninput(element, value, status, render_template, msg=''): def _get_render_context(self):
"""
Select option input type.
Example: context = {
'id': self.id,
'value': self.value,
'status': self.status,
'msg': self.msg,
'options': self.osetdict,
'inline': self.xml.get('inline',''),
}
return context
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text> registry.register(OptionInput)
"""
eid = element.get('id')
options = element.get('options')
if not options:
raise Exception(
"[courseware.capa.inputtypes.optioninput] Missing options specification in "
+ etree.tostring(element))
# parse the set of possible options
oset = shlex.shlex(options[1:-1])
oset.quotes = "'"
oset.whitespace = ","
oset = [x[1:-1] for x in list(oset)]
# make ordered list with (key, value) same
osetdict = [(oset[x], oset[x]) for x in range(len(oset))]
# TODO: allow ordering to be randomized
context = {'id': eid,
'value': value,
'state': status,
'msg': msg,
'options': osetdict,
'inline': element.get('inline',''),
}
return context
register_input_class(OptionInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
# desired semantics. # desired semantics.
# @register_render_function
def choicegroup(element, value, status, render_template, msg=''): class ChoiceGroup(InputTypeBase):
''' """
Radio button inputs: multiple choice or true/false Radio button or checkbox inputs: multiple choice or true/false
TODO: allow order of choices to be randomized, following lon-capa spec. Use TODO: allow order of choices to be randomized, following lon-capa spec. Use
"location" attribute, ie random, top, bottom. "location" attribute, ie random, top, bottom.
'''
eid = element.get('id')
if element.get('type') == "MultipleChoice":
element_type = "radio"
elif element.get('type') == "TrueFalse":
element_type = "checkbox"
else:
element_type = "radio"
choices = []
for choice in element:
if not choice.tag == 'choice':
raise Exception("[courseware.capa.inputtypes.choicegroup] "
"Error: only <choice> tags should be immediate children "
"of a <choicegroup>, found %s instead" % choice.tag)
ctext = ""
# TODO: what if choice[0] has math tags in it?
ctext += ''.join([etree.tostring(x) for x in choice])
if choice.text is not None:
# TODO: fix order?
ctext += choice.text
choices.append((choice.get("name"), ctext))
context = {'id': eid,
'value': value,
'state': status,
'input_type': element_type,
'choices': choices,
'name_array_suffix': ''}
html = render_template("choicegroup.html", context)
return etree.XML(html)
_reg(choicegroup) Example:
<choicegroup>
<choice correct="false" name="foil1">
<text>This is foil One.</text>
</choice>
<choice correct="false" name="foil2">
<text>This is foil Two.</text>
</choice>
<choice correct="true" name="foil3">
<text>This is foil Three.</text>
</choice>
</choicegroup>
"""
template = "choicegroup.html"
tags = ['choicegroup', 'radiogroup', 'checkboxgroup']
def setup(self):
# suffix is '' or [] to change the way the input is handled in --as a scalar or vector
# value. (VS: would be nice to make this less hackish).
if self.tag == 'choicegroup':
self.suffix = ''
self.element_type = "radio"
elif self.tag == 'radiogroup':
self.element_type = "radio"
self.suffix = '[]'
elif self.tag == 'checkboxgroup':
self.element_type = "checkbox"
self.suffix = '[]'
else:
raise Exception("ChoiceGroup: unexpected tag {0}".format(self.tag))
self.choices = extract_choices(self.xml)
def _get_render_context(self):
context = {'id': self.id,
'value': self.value,
'status': self.status,
'input_type': self.element_type,
'choices': self.choices,
'name_array_suffix': self.suffix}
return context
#-----------------------------------------------------------------------------
def extract_choices(element): def extract_choices(element):
''' '''
Extracts choices for a few input types, such as radiogroup and Extracts choices for a few input types, such as ChoiceGroup, RadioGroup and
checkboxgroup. CheckboxGroup.
returns list of (choice_name, choice_text) tuples
TODO: allow order of choices to be randomized, following lon-capa spec. Use TODO: allow order of choices to be randomized, following lon-capa spec. Use
"location" attribute, ie random, top, bottom. "location" attribute, ie random, top, bottom.
...@@ -297,380 +258,258 @@ def extract_choices(element): ...@@ -297,380 +258,258 @@ def extract_choices(element):
choices = [] choices = []
for choice in element: for choice in element:
if not choice.tag == 'choice': if choice.tag != 'choice':
raise Exception("[courseware.capa.inputtypes.extract_choices] \ raise Exception(
Expected a <choice> tag; got %s instead" "[capa.inputtypes.extract_choices] Expected a <choice> tag; got %s instead"
% choice.tag) % choice.tag)
choice_text = ''.join([etree.tostring(x) for x in choice]) choice_text = ''.join([etree.tostring(x) for x in choice])
if choice.text is not None:
# TODO: fix order?
choice_text += choice.text
choices.append((choice.get("name"), choice_text)) choices.append((choice.get("name"), choice_text))
return choices return choices
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of registry.register(ChoiceGroup)
# desired semantics.
def radiogroup(element, value, status, render_template, msg=''):
'''
Radio button inputs: (multiple choice)
'''
eid = element.get('id')
choices = extract_choices(element) #-----------------------------------------------------------------------------
context = {'id': eid,
'value': value,
'state': status,
'input_type': 'radio',
'choices': choices,
'name_array_suffix': '[]'}
html = render_template("choicegroup.html", context) class JavascriptInput(InputTypeBase):
return etree.XML(html) """
Hidden field for javascript to communicate via; also loads the required
scripts for rendering the problem and passes data to the problem.
TODO (arjun?): document this in detail. Initial notes:
- display_class is a subclass of XProblemClassDisplay (see
xmodule/xmodule/js/src/capa/display.coffee),
- display_file is the js script to be in /static/js/ where display_class is defined.
"""
_reg(radiogroup) template = "javascriptinput.html"
tags = ['javascriptinput']
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of def setup(self):
# desired semantics. # Need to provide a value that JSON can parse if there is no
def checkboxgroup(element, value, status, render_template, msg=''): # student-supplied value yet.
''' if self.value == "":
Checkbox inputs: (select one or more choices) self.value = 'null'
'''
eid = element.get('id') self.params = self.xml.get('params')
self.problem_state = self.xml.get('problem_state')
self.display_class = self.xml.get('display_class')
self.display_file = self.xml.get('display_file')
choices = extract_choices(element)
context = {'id': eid, def _get_render_context(self):
escapedict = {'"': '&quot;'}
value = saxutils.escape(self.value, escapedict)
msg = saxutils.escape(self.msg, escapedict)
context = {'id': self.id,
'params': self.params,
'display_file': self.display_file,
'display_class': self.display_class,
'problem_state': self.problem_state,
'value': value, 'value': value,
'state': status, 'evaluation': msg,
'input_type': 'checkbox', }
'choices': choices, return context
'name_array_suffix': '[]'}
html = render_template("choicegroup.html", context) registry.register(JavascriptInput)
return etree.XML(html)
_reg(checkboxgroup)
def javascriptinput(element, value, status, render_template, msg='null'): #-----------------------------------------------------------------------------
'''
Hidden field for javascript to communicate via; also loads the required
scripts for rendering the problem and passes data to the problem.
'''
eid = element.get('id')
params = element.get('params')
problem_state = element.get('problem_state')
display_class = element.get('display_class')
display_file = element.get('display_file')
# Need to provide a value that JSON can parse if there is no
# student-supplied value yet.
if value == "":
value = 'null'
escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict)
msg = saxutils.escape(msg, escapedict)
context = {'id': eid,
'params': params,
'display_file': display_file,
'display_class': display_class,
'problem_state': problem_state,
'value': value,
'evaluation': msg,
}
html = render_template("javascriptinput.html", context)
return etree.XML(html)
_reg(javascriptinput) class TextLine(InputTypeBase):
"""
"""
def textline(element, value, status, render_template, msg=""): template = "textline.html"
''' tags = ['textline']
Simple text line input, with optional size specification.
'''
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
if element.get('math') or element.get('dojs'):
return textline_dynamath(element, value, status, render_template, msg)
eid = element.get('id')
if eid is None:
msg = 'textline has no id: it probably appears outside of a known response type'
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
raise Exception(msg)
count = int(eid.split('_')[-2]) - 1 # HACK
size = element.get('size')
# if specified, then textline is hidden and id is stored in div of name given by hidden
hidden = element.get('hidden', '')
# Escape answers with quotes, so they don't crash the system!
escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict)
context = {'id': eid,
'value': value,
'state': status,
'count': count,
'size': size,
'msg': msg,
'hidden': hidden,
'inline': element.get('inline',''),
}
html = render_template("textinput.html", context) def setup(self):
try: self.size = self.xml.get('size')
xhtml = etree.XML(html)
except Exception as err:
# TODO: needs to be self.system.DEBUG - but can't access system
if True:
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
raise
return xhtml
_reg(textline) # if specified, then textline is hidden and input id is stored
# in div with name=self.hidden.
self.hidden = self.xml.get('hidden', False)
#----------------------------------------------------------------------------- self.inline = self.xml.get('inline', False)
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
self.do_math = bool(self.xml.get('math') or self.xml.get('dojs'))
# TODO: do math checking using ajax instead of using js, so
# that we only have one math parser.
self.preprocessor = None
if self.do_math:
# Preprocessor to insert between raw input and Mathjax
self.preprocessor = {'class_name': self.xml.get('preprocessorClassName',''),
'script_src': self.xml.get('preprocessorSrc','')}
if '' in self.preprocessor.values():
self.preprocessor = None
def textline_dynamath(element, value, status, render_template, msg=''):
'''
Text line input with dynamic math display (equation rendered on client in real time
during input).
'''
# TODO: Make a wrapper for <formulainput>
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
'''
textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
uses a <span id=display_eid>`{::}`</span>
and a hidden textarea with id=input_eid_fromjs for the mathjax rendering and return.
'''
eid = element.get('id')
count = int(eid.split('_')[-2]) - 1 # HACK
size = element.get('size')
# if specified, then textline is hidden and id is stored in div of name given by hidden
hidden = element.get('hidden', '')
# Preprocessor to insert between raw input and Mathjax
preprocessor = {'class_name': element.get('preprocessorClassName',''),
'script_src': element.get('preprocessorSrc','')}
if '' in preprocessor.values():
preprocessor = None
# Escape characters in student input for safe XML parsing
escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict)
context = {'id': eid,
'value': value,
'state': status,
'count': count,
'size': size,
'msg': msg,
'hidden': hidden,
'preprocessor': preprocessor,}
html = render_template("textinput_dynamath.html", context)
return etree.XML(html)
#----------------------------------------------------------------------------- def _get_render_context(self):
def filesubmission(element, value, status, render_template, msg=''): # Escape answers with quotes, so they don't crash the system!
''' escapedict = {'"': '&quot;'}
Upload a single file (e.g. for programming assignments) value = saxutils.escape(self.value, escapedict)
'''
eid = element.get('id') context = {'id': self.id,
escapedict = {'"': '&quot;'} 'value': value,
allowed_files = json.dumps(element.get('allowed_files', '').split()) 'status': self.status,
allowed_files = saxutils.escape(allowed_files, escapedict) 'size': self.size,
required_files = json.dumps(element.get('required_files', '').split()) 'msg': self.msg,
required_files = saxutils.escape(required_files, escapedict) 'hidden': self.hidden,
'inline': self.inline,
# Check if problem has been queued 'do_math': self.do_math,
queue_len = 0 'preprocessor': self.preprocessor,
# Flag indicating that the problem has been queued, 'msg' is length of queue }
if status == 'incomplete': return context
status = 'queued'
queue_len = msg
msg = 'Submitted to grader.'
context = { 'id': eid,
'state': status,
'msg': msg,
'value': value,
'queue_len': queue_len,
'allowed_files': allowed_files,
'required_files': required_files,}
html = render_template("filesubmission.html", context)
return etree.XML(html)
_reg(filesubmission)
registry.register(TextLine)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
## TODO: Make a wrapper for <codeinput>
def textbox(element, value, status, render_template, msg=''):
'''
The textbox is used for code input. The message is the return HTML string from
evaluating the code, eg error messages, and output from the code tests.
''' class FileSubmission(InputTypeBase):
eid = element.get('id') """
count = int(eid.split('_')[-2]) - 1 # HACK Upload some files (e.g. for programming assignments)
size = element.get('size') """
rows = element.get('rows') or '30'
cols = element.get('cols') or '80'
# if specified, then textline is hidden and id is stored in div of name given by hidden
hidden = element.get('hidden', '')
# if no student input yet, then use the default input given by the problem
if not value:
value = element.text
# Check if problem has been queued
queue_len = 0
# Flag indicating that the problem has been queued, 'msg' is length of queue
if status == 'incomplete':
status = 'queued'
queue_len = msg
msg = 'Submitted to grader.'
# For CodeMirror
mode = element.get('mode','python')
linenumbers = element.get('linenumbers','true')
tabsize = element.get('tabsize','4')
tabsize = int(tabsize)
context = {'id': eid,
'value': value,
'state': status,
'count': count,
'size': size,
'msg': msg,
'mode': mode,
'linenumbers': linenumbers,
'rows': rows,
'cols': cols,
'hidden': hidden,
'tabsize': tabsize,
'queue_len': queue_len,
}
html = render_template("textbox.html", context)
try:
xhtml = etree.XML(html)
except Exception as err:
newmsg = 'error %s in rendering message' % (str(err).replace('<', '&lt;'))
newmsg += '<br/>Original message: %s' % msg.replace('<', '&lt;')
context['msg'] = newmsg
html = render_template("textbox.html", context)
xhtml = etree.XML(html)
return xhtml
template = "filesubmission.html"
tags = ['filesubmission']
_reg(textbox) # pulled out for testing
submitted_msg = ("Your file(s) have been submitted; as soon as your submission is"
" graded, this message will be replaced with the grader's feedback.")
def setup(self):
escapedict = {'"': '&quot;'}
self.allowed_files = json.dumps(self.xml.get('allowed_files', '').split())
self.allowed_files = saxutils.escape(self.allowed_files, escapedict)
self.required_files = json.dumps(self.xml.get('required_files', '').split())
self.required_files = saxutils.escape(self.required_files, escapedict)
# Check if problem has been queued
self.queue_len = 0
# Flag indicating that the problem has been queued, 'msg' is length of queue
if self.status == 'incomplete':
self.status = 'queued'
self.queue_len = self.msg
self.msg = FileSubmission.submitted_msg
def _get_render_context(self):
context = {'id': self.id,
'status': self.status,
'msg': self.msg,
'value': self.value,
'queue_len': self.queue_len,
'allowed_files': self.allowed_files,
'required_files': self.required_files,}
return context
registry.register(FileSubmission)
#-----------------------------------------------------------------------------
def schematic(element, value, status, render_template, msg=''):
eid = element.get('id')
height = element.get('height')
width = element.get('width')
parts = element.get('parts')
analyses = element.get('analyses')
initial_value = element.get('initial_value')
submit_analyses = element.get('submit_analyses')
context = {
'id': eid,
'value': value,
'initial_value': initial_value,
'state': status,
'width': width,
'height': height,
'parts': parts,
'analyses': analyses,
'submit_analyses': submit_analyses,
}
html = render_template("schematicinput.html", context)
return etree.XML(html)
_reg(schematic)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
### TODO: Move out of inputtypes
def math(element, value, status, render_template, msg=''):
'''
This is not really an input type. It is a convention from Lon-CAPA, used for
displaying a math equation.
Examples: class CodeInput(InputTypeBase):
"""
A text area input for code--uses codemirror, does syntax highlighting, special tab handling,
etc.
"""
<m display="jsmath">$\displaystyle U(r)=4 U_0 </m> template = "codeinput.html"
<m>$r_0$</m> tags = ['codeinput',
'textbox', # Another (older) name--at some point we may want to make it use a
# non-codemirror editor.
]
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
TODO: use shorter tags (but this will require converting problem XML files!) def setup(self):
''' self.rows = self.xml.get('rows') or '30'
mathstr = re.sub('\$(.*)\$', '[mathjaxinline]\\1[/mathjaxinline]', element.text) self.cols = self.xml.get('cols') or '80'
mtag = 'mathjax' # if specified, then textline is hidden and id is stored in div of name given by hidden
if not '\\displaystyle' in mathstr: mtag += 'inline' self.hidden = self.xml.get('hidden', '')
else: mathstr = mathstr.replace('\\displaystyle', '')
mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag)
#if '\\displaystyle' in mathstr:
# isinline = False
# mathstr = mathstr.replace('\\displaystyle','')
#else:
# isinline = True
# html = render_template("mathstring.html", {'mathstr':mathstr,
# 'isinline':isinline,'tail':element.tail})
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail))
try:
xhtml = etree.XML(html)
except Exception as err:
if False: # TODO needs to be self.system.DEBUG - but can't access system
msg = '<html><div class="inline-error"><p>Error %s</p>' % str(err).replace('<', '&lt;')
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
html.replace('<', '&lt;'))
msg += "</div></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
# xhtml.tail = element.tail # don't forget to include the tail!
return xhtml
_reg(math) # if no student input yet, then use the default input given by the problem
if not self.value:
self.value = self.xml.text
#----------------------------------------------------------------------------- # Check if problem has been queued
self.queue_len = 0
# Flag indicating that the problem has been queued, 'msg' is length of queue
if self.status == 'incomplete':
self.status = 'queued'
self.queue_len = self.msg
self.msg = 'Submitted to grader.'
# For CodeMirror
self.mode = self.xml.get('mode', 'python')
self.linenumbers = self.xml.get('linenumbers', 'true')
self.tabsize = int(self.xml.get('tabsize', '4'))
def solution(element, value, status, render_template, msg=''): def _get_render_context(self):
'''
This is not really an input type. It is just a <span>...</span> which is given an ID, context = {'id': self.id,
that is used for displaying an extended answer (a problem "solution") after "show answers" 'value': self.value,
is pressed. Note that the solution content is NOT sent with the HTML. It is obtained 'status': self.status,
by an ajax call. 'msg': self.msg,
''' 'mode': self.mode,
eid = element.get('id') 'linenumbers': self.linenumbers,
size = element.get('size') 'rows': self.rows,
context = {'id': eid, 'cols': self.cols,
'value': value, 'hidden': self.hidden,
'state': status, 'tabsize': self.tabsize,
'size': size, 'queue_len': self.queue_len,
'msg': msg,
} }
html = render_template("solutionspan.html", context) return context
return etree.XML(html)
registry.register(CodeInput)
_reg(solution)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class Schematic(InputTypeBase):
"""
"""
template = "schematicinput.html"
tags = ['schematic']
def imageinput(element, value, status, render_template, msg=''): def setup(self):
''' self.height = self.xml.get('height')
self.width = self.xml.get('width')
self.parts = self.xml.get('parts')
self.analyses = self.xml.get('analyses')
self.initial_value = self.xml.get('initial_value')
self.submit_analyses = self.xml.get('submit_analyses')
def _get_render_context(self):
context = {'id': self.id,
'value': self.value,
'initial_value': self.initial_value,
'status': self.status,
'width': self.width,
'height': self.height,
'parts': self.parts,
'analyses': self.analyses,
'submit_analyses': self.submit_analyses,}
return context
registry.register(Schematic)
#-----------------------------------------------------------------------------
class ImageInput(InputTypeBase):
"""
Clickable image as an input field. Element should specify the image source, height, Clickable image as an input field. Element should specify the image source, height,
and width, e.g. and width, e.g.
...@@ -678,79 +517,117 @@ def imageinput(element, value, status, render_template, msg=''): ...@@ -678,79 +517,117 @@ def imageinput(element, value, status, render_template, msg=''):
TODO: showanswer for imageimput does not work yet - need javascript to put rectangle TODO: showanswer for imageimput does not work yet - need javascript to put rectangle
over acceptable area of image. over acceptable area of image.
''' """
eid = element.get('id')
src = element.get('src') template = "imageinput.html"
height = element.get('height') tags = ['imageinput']
width = element.get('width')
def setup(self):
# if value is of the form [x,y] then parse it and send along coordinates of previous answer self.src = self.xml.get('src')
m = re.match('\[([0-9]+),([0-9]+)]', value.strip().replace(' ', '')) self.height = self.xml.get('height')
if m: self.width = self.xml.get('width')
(gx, gy) = [int(x) - 15 for x in m.groups()]
else: # if value is of the form [x,y] then parse it and send along coordinates of previous answer
(gx, gy) = (0, 0) m = re.match('\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', ''))
if m:
context = { # Note: we subtract 15 to compensate for the size of the dot on the screen.
'id': eid, # (is a 30x30 image--lms/static/green-pointer.png).
'value': value, (self.gx, self.gy) = [int(x) - 15 for x in m.groups()]
'height': height, else:
'width': width, (self.gx, self.gy) = (0, 0)
'src': src,
'gx': gx,
'gy': gy, def _get_render_context(self):
'state': status, # to change
'msg': msg, # to change context = {'id': self.id,
} 'value': self.value,
html = render_template("imageinput.html", context) 'height': self.height,
return etree.XML(html) 'width': self.width,
'src': self.src,
_reg(imageinput) 'gx': self.gx,
'gy': self.gy,
'status': self.status,
'msg': self.msg,
}
return context
registry.register(ImageInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
def crystallography(element, value, status, render_template, msg=''):
eid = element.get('id') class Crystallography(InputTypeBase):
if eid is None: """
msg = 'cryst has no id: it probably appears outside of a known response type' An input for crystallography -- user selects 3 points on the axes, and we get a plane.
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
raise Exception(msg) TODO: what's the actual value format?
height = element.get('height') """
width = element.get('width')
display_file = element.get('display_file') template = "crystallography.html"
tags = ['crystallography']
count = int(eid.split('_')[-2]) - 1 # HACK
size = element.get('size')
# if specified, then textline is hidden and id is stored in div of name given by hidden def setup(self):
hidden = element.get('hidden', '') self.height = self.xml.get('height')
# Escape answers with quotes, so they don't crash the system! self.width = self.xml.get('width')
escapedict = {'"': '&quot;'} self.size = self.xml.get('size')
value = saxutils.escape(value, escapedict)
# if specified, then textline is hidden and id is stored in div of name given by hidden
context = {'id': eid, self.hidden = self.xml.get('hidden', '')
'value': value,
'state': status, # Escape answers with quotes, so they don't crash the system!
'count': count, escapedict = {'"': '&quot;'}
'size': size, self.value = saxutils.escape(self.value, escapedict)
'msg': msg,
'hidden': hidden, def _get_render_context(self):
'inline': element.get('inline', ''), context = {'id': self.id,
'width': width, 'value': self.value,
'height': height, 'status': self.status,
'display_file': display_file, 'size': self.size,
'msg': self.msg,
'hidden': self.hidden,
'width': self.width,
'height': self.height,
} }
return context
html = render_template("crystallography.html", context) registry.register(Crystallography)
try:
xhtml = etree.XML(html)
except Exception as err:
# TODO: needs to be self.system.DEBUG - but can't access system
if True:
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
raise
return xhtml
_reg(crystallography) # -------------------------------------------------------------------------
class VseprInput(InputTypeBase):
"""
Input for molecular geometry--show possible structures, let student
pick structure and label positions with atoms or electron pairs.
"""
template = 'vsepr_input.html'
tags = ['vsepr_input']
def setup(self):
self.height = self.xml.get('height')
self.width = self.xml.get('width')
# Escape answers with quotes, so they don't crash the system!
escapedict = {'"': '&quot;'}
self.value = saxutils.escape(self.value, escapedict)
self.molecules = self.xml.get('molecules')
self.geometries = self.xml.get('geometries')
def _get_render_context(self):
context = {'id': self.id,
'value': self.value,
'status': self.status,
'msg': self.msg,
'width': self.width,
'height': self.height,
'molecules': self.molecules,
'geometries': self.geometries,
}
return context
registry.register(VseprInput)
#-------------------------------------------------------------------------------- #--------------------------------------------------------------------------------
...@@ -769,15 +646,17 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -769,15 +646,17 @@ class ChemicalEquationInput(InputTypeBase):
template = "chemicalequationinput.html" template = "chemicalequationinput.html"
tags = ['chemicalequationinput'] tags = ['chemicalequationinput']
def setup(self):
self.size = self.xml.get('size', '20')
def _get_render_context(self): def _get_render_context(self):
size = self.xml.get('size', '20')
context = { context = {
'id': self.id, 'id': self.id,
'value': self.value, 'value': self.value,
'status': self.status, 'status': self.status,
'size': size, 'size': self.size,
'previewer': '/static/js/capa/chemical_equation_preview.js', 'previewer': '/static/js/capa/chemical_equation_preview.js',
} }
return context return context
register_input_class(ChemicalEquationInput) registry.register(ChemicalEquationInput)
class TagRegistry(object):
"""
A registry mapping tags to handlers.
(A dictionary with some extra error checking.)
"""
def __init__(self):
self._mapping = {}
def register(self, cls):
"""
Register cls as a supported tag type. It is expected to define cls.tags as a list of tags
that it implements.
If an already-registered type has registered one of those tags, will raise ValueError.
If there are no tags in cls.tags, will also raise ValueError.
"""
# Do all checks and complain before changing any state.
if len(cls.tags) == 0:
raise ValueError("No tags specified for class {0}".format(cls.__name__))
for t in cls.tags:
if t in self._mapping:
other_cls = self._mapping[t]
if cls == other_cls:
# registering the same class multiple times seems silly, but ok
continue
raise ValueError("Tag {0} already registered by class {1}."
" Can't register for class {2}"
.format(t, other_cls.__name__, cls.__name__))
# Ok, should be good to change state now.
for t in cls.tags:
self._mapping[t] = cls
def registered_tags(self):
"""
Get a list of all the tags that have been registered.
"""
return self._mapping.keys()
def get_class_for_tag(self, tag):
"""
For any tag in registered_tags(), returns the corresponding class. Otherwise, will raise
KeyError.
"""
return self._mapping[tag]
...@@ -81,7 +81,7 @@ class LoncapaResponse(object): ...@@ -81,7 +81,7 @@ class LoncapaResponse(object):
by __init__ by __init__
- check_hint_condition : check to see if the student's answers satisfy a particular - check_hint_condition : check to see if the student's answers satisfy a particular
condition for a hint to be displayed condition for a hint to be displayed
- render_html : render this Response as HTML (must return XHTML-compliant string) - render_html : render this Response as HTML (must return XHTML-compliant string)
- __unicode__ : unicode representation of this Response - __unicode__ : unicode representation of this Response
...@@ -148,6 +148,7 @@ class LoncapaResponse(object): ...@@ -148,6 +148,7 @@ class LoncapaResponse(object):
# for convenience # for convenience
self.answer_id = self.answer_ids[0] self.answer_id = self.answer_ids[0]
# map input_id -> maxpoints
self.maxpoints = dict() self.maxpoints = dict()
for inputfield in self.inputfields: for inputfield in self.inputfields:
# By default, each answerfield is worth 1 point # By default, each answerfield is worth 1 point
...@@ -284,17 +285,14 @@ class LoncapaResponse(object): ...@@ -284,17 +285,14 @@ class LoncapaResponse(object):
(correctness, npoints, msg) for each answer_id. (correctness, npoints, msg) for each answer_id.
Arguments: Arguments:
- student_answers : dict of (answer_id,answer) where answer = student input (string) - student_answers : dict of (answer_id, answer) where answer = student input (string)
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or
recording history of responses
''' '''
pass pass
@abc.abstractmethod @abc.abstractmethod
def get_answers(self): def get_answers(self):
''' '''
Return a dict of (answer_id,answer_text) for each answer for this question. Return a dict of (answer_id, answer_text) for each answer for this question.
''' '''
pass pass
...@@ -871,7 +869,8 @@ def sympy_check2(): ...@@ -871,7 +869,8 @@ def sympy_check2():
</customresponse>"""}] </customresponse>"""}]
response_tag = 'customresponse' response_tag = 'customresponse'
allowed_inputfields = ['textline', 'textbox', 'crystallography', 'chemicalequationinput']
allowed_inputfields = ['textline', 'textbox', 'crystallography', 'chemicalequationinput', 'vsepr_input']
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
...@@ -1720,7 +1719,7 @@ class ImageResponse(LoncapaResponse): ...@@ -1720,7 +1719,7 @@ class ImageResponse(LoncapaResponse):
""" """
Handle student response for image input: the input is a click on an image, Handle student response for image input: the input is a click on an image,
which produces an [x,y] coordinate pair. The click is correct if it falls which produces an [x,y] coordinate pair. The click is correct if it falls
within a region specified. This region is nominally a rectangle. within a region specified. This region is a union of rectangles.
Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That Lon-CAPA requires that each <imageresponse> has a <foilgroup> inside it. That
doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse> doesn't make sense to me (Ike). Instead, let's have it such that <imageresponse>
...@@ -1730,6 +1729,7 @@ class ImageResponse(LoncapaResponse): ...@@ -1730,6 +1729,7 @@ class ImageResponse(LoncapaResponse):
snippets = [{'snippet': '''<imageresponse> snippets = [{'snippet': '''<imageresponse>
<imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" /> <imageinput src="image1.jpg" width="200" height="100" rectangle="(10,10)-(20,30)" />
<imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" /> <imageinput src="image2.jpg" width="210" height="130" rectangle="(12,12)-(40,60)" />
<imageinput src="image2.jpg" width="210" height="130" rectangle="(10,10)-(20,30);(12,12)-(40,60)" />
</imageresponse>'''}] </imageresponse>'''}]
response_tag = 'imageresponse' response_tag = 'imageresponse'
...@@ -1746,20 +1746,10 @@ class ImageResponse(LoncapaResponse): ...@@ -1746,20 +1746,10 @@ class ImageResponse(LoncapaResponse):
for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza for aid in self.answer_ids: # loop through IDs of <imageinput> fields in our stanza
given = student_answers[aid] # this should be a string of the form '[x,y]' given = student_answers[aid] # this should be a string of the form '[x,y]'
correct_map.set(aid, 'incorrect')
if not given: # No answer to parse. Mark as incorrect and move on if not given: # No answer to parse. Mark as incorrect and move on
correct_map.set(aid, 'incorrect')
continue continue
# parse expected answer
# TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
expectedset[aid].strip().replace(' ', ''))
if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
# parse given answer # parse given answer
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', '')) m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m: if not m:
...@@ -1767,11 +1757,24 @@ class ImageResponse(LoncapaResponse): ...@@ -1767,11 +1757,24 @@ class ImageResponse(LoncapaResponse):
'error grading %s (input=%s)' % (aid, given)) 'error grading %s (input=%s)' % (aid, given))
(gx, gy) = [int(x) for x in m.groups()] (gx, gy) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle # Check whether given point lies in any of the solution rectangles
if (llx <= gx <= urx) and (lly <= gy <= ury): solution_rectangles = expectedset[aid].split(';')
correct_map.set(aid, 'correct') for solution_rectangle in solution_rectangles:
else: # parse expected answer
correct_map.set(aid, 'incorrect') # TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
solution_rectangle.strip().replace(' ', ''))
if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle
if (llx <= gx <= urx) and (lly <= gy <= ury):
correct_map.set(aid, 'correct')
break
return correct_map return correct_map
def get_answers(self): def get_answers(self):
......
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
<div class="indicator_container"> <div class="indicator_container">
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
</div> </div>
......
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
>${value|h}</textarea> >${value|h}</textarea>
<div class="grader-status"> <div class="grader-status">
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}">Correct</span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<p class="debug">${state}</p> <p class="debug">${status}</p>
</div> </div>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
......
<% doinline = "inline" if inline else "" %> <section id="inputtype_${id}" class="capa_inputtype" >
<section id="textinput_${id}" class="textinput ${doinline}" >
<div id="holder" style="width:${width};height:${height}"></div> <div id="holder" style="width:${width};height:${height}"></div>
<div class="script_placeholder" data-src="/static/js/raphael.js"></div><div class="script_placeholder" data-src="/static/js/sylvester.js"></div><div class="script_placeholder" data-src="/static/js/underscore-min.js"></div> <div class="script_placeholder" data-src="/static/js/raphael.js"></div>
<div class="script_placeholder" data-src="/static/js/sylvester.js"></div>
<div class="script_placeholder" data-src="/static/js/underscore-min.js"></div>
<div class="script_placeholder" data-src="/static/js/crystallography.js"></div> <div class="script_placeholder" data-src="/static/js/crystallography.js"></div>
% if state == 'unsubmitted': % if status == 'unsubmitted':
<div class="unanswered ${doinline}" id="status_${id}"> <div class="unanswered" id="status_${id}">
% elif state == 'correct': % elif status == 'correct':
<div class="correct ${doinline}" id="status_${id}"> <div class="correct" id="status_${id}">
% elif state == 'incorrect': % elif status == 'incorrect':
<div class="incorrect ${doinline}" id="status_${id}"> <div class="incorrect" id="status_${id}">
% elif state == 'incomplete': % elif status == 'incomplete':
<div class="incorrect ${doinline}" id="status_${id}"> <div class="incorrect" id="status_${id}">
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
...@@ -29,13 +29,13 @@ ...@@ -29,13 +29,13 @@
/> />
<p class="status"> <p class="status">
% if state == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif state == 'correct': % elif status == 'correct':
correct correct
% elif state == 'incorrect': % elif status == 'incorrect':
incorrect incorrect
% elif state == 'incomplete': % elif status == 'incomplete':
incomplete incomplete
% endif % endif
</p> </p>
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</section> </section>
<section id="filesubmission_${id}" class="filesubmission"> <section id="filesubmission_${id}" class="filesubmission">
<div class="grader-status file"> <div class="grader-status file">
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}">Correct</span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
<p class="debug">${state}</p> <p class="debug">${status}</p>
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/> <input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/>
</div> </div>
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
<img src="/static/green-pointer.png" id="cross_${id}" style="position: absolute;top: ${gy}px;left: ${gx}px;" /> <img src="/static/green-pointer.png" id="cross_${id}" style="position: absolute;top: ${gy}px;left: ${gx}px;" />
</div> </div>
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
</span> </span>
...@@ -18,13 +18,13 @@ ...@@ -18,13 +18,13 @@
<textarea style="display:none" id="input_${id}_fromjs" name="input_${id}_fromjs"></textarea> <textarea style="display:none" id="input_${id}_fromjs" name="input_${id}_fromjs"></textarea>
% endif % endif
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
% if msg: % if msg:
......
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
</form> </form>
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
</script> </script>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span>
% endif % endif
</span> </span>
......
###
### version of textline.html which does dynamic math
###
<section class="text-input-dynamath capa_inputtype" id="inputtype_${id}">
% if preprocessor is not None:
<div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/>
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
% endif
% if state == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif state == 'correct':
<div class="correct" id="status_${id}">
% elif state == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif state == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}" class="math" size="${size if size else ''}"
% if hidden:
style="display:none;"
% endif
/>
<p class="status">
% if state == 'unsubmitted':
unanswered
% elif state == 'correct':
correct
% elif state == 'incorrect':
incorrect
% elif state == 'incomplete':
incomplete
% endif
</p>
<p id="answer_${id}" class="answer"></p>
<div id="display_${id}" class="equation">`{::}`</div>
</div>
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> </textarea>
% if msg:
<span class="message">${msg|n}</span>
% endif
</section>
<% doinline = "inline" if inline else "" %> <% doinline = "inline" if inline else "" %>
<section id="textinput_${id}" class="textinput ${doinline}" > <section id="inputtype_${id}" class="${'text-input-dynamath' if do_math else ''} capa_inputtype ${doinline}" >
% if state == 'unsubmitted':
% if preprocessor is not None:
<div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/>
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
% endif
% if status == 'unsubmitted':
<div class="unanswered ${doinline}" id="status_${id}"> <div class="unanswered ${doinline}" id="status_${id}">
% elif state == 'correct': % elif status == 'correct':
<div class="correct ${doinline}" id="status_${id}"> <div class="correct ${doinline}" id="status_${id}">
% elif state == 'incorrect': % elif status == 'incorrect':
<div class="incorrect ${doinline}" id="status_${id}"> <div class="incorrect ${doinline}" id="status_${id}">
% elif state == 'incomplete': % elif status == 'incomplete':
<div class="incorrect ${doinline}" id="status_${id}"> <div class="incorrect ${doinline}" id="status_${id}">
% endif % endif
% if hidden: % if hidden:
...@@ -15,32 +21,44 @@ ...@@ -15,32 +21,44 @@
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}" <input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size: % if do_math:
size="${size}" class="math"
% endif % endif
% if hidden: % if size:
style="display:none;" size="${size}"
% endif % endif
% if hidden:
style="display:none;"
% endif
/> />
<p class="status"> <p class="status">
% if state == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif state == 'correct': % elif status == 'correct':
correct correct
% elif state == 'incorrect': % elif status == 'incorrect':
incorrect incorrect
% elif state == 'incomplete': % elif status == 'incomplete':
incomplete incomplete
% endif % endif
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if do_math:
<div id="display_${id}" class="equation">`{::}`</div>
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath">
</textarea>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden:
</div>
% endif
</section> </section>
<section id="inputtype_${id}" class="capa_inputtype" >
<table><tr><td height='600'>
<div id="vsepr_div_${id}" style="position:relative;" data-molecules="${molecules}" data-geometries="${geometries}">
<canvas id="vsepr_canvas_${id}" width="${width}" height="${height}">
</canvas>
</div>
</td><td valign ='top'>
<select class="molecule_select" id="molecule_select_${id}" size="18">
</select>
</td></tr></table>
<div class="script_placeholder" data-src="/static/js/vsepr/vsepr.js"></div>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
style="display:none;"
/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${msg|n}</span>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
...@@ -4,13 +4,23 @@ import os ...@@ -4,13 +4,23 @@ import os
from mock import Mock from mock import Mock
import xml.sax.saxutils as saxutils
TEST_DIR = os.path.dirname(os.path.realpath(__file__)) TEST_DIR = os.path.dirname(os.path.realpath(__file__))
def tst_render_template(template, context):
"""
A test version of render to template. Renders to the repr of the context, completely ignoring
the template name. To make the output valid xml, quotes the content, and wraps it in a <div>
"""
return '<div>{0}</div>'.format(saxutils.escape(repr(context)))
test_system = Mock( test_system = Mock(
ajax_url='courses/course_id/modx/a_location', ajax_url='courses/course_id/modx/a_location',
track_function=Mock(), track_function=Mock(),
get_module=Mock(), get_module=Mock(),
render_template=Mock(), render_template=tst_render_template,
replace_urls=Mock(), replace_urls=Mock(),
user=Mock(), user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")), filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
......
from lxml import etree
import unittest
import xml.sax.saxutils as saxutils
from . import test_system
from capa import customrender
# just a handy shortcut
lookup_tag = customrender.registry.get_class_for_tag
def extract_context(xml):
"""
Given an xml element corresponding to the output of test_system.render_template, get back the
original context
"""
return eval(xml.text)
def quote_attr(s):
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
class HelperTest(unittest.TestCase):
'''
Make sure that our helper function works!
'''
def check(self, d):
xml = etree.XML(test_system.render_template('blah', d))
self.assertEqual(d, extract_context(xml))
def test_extract_context(self):
self.check({})
self.check({1, 2})
self.check({'id', 'an id'})
self.check({'with"quote', 'also"quote'})
class SolutionRenderTest(unittest.TestCase):
'''
Make sure solutions render properly.
'''
def test_rendering(self):
solution = 'To compute unicorns, count them.'
xml_str = """<solution id="solution_12">{s}</solution>""".format(s=solution)
element = etree.fromstring(xml_str)
renderer = lookup_tag('solution')(test_system, element)
self.assertEqual(renderer.id, 'solution_12')
# our test_system "renders" templates to a div with the repr of the context
xml = renderer.get_html()
context = extract_context(xml)
self.assertEqual(context, {'id' : 'solution_12'})
class MathRenderTest(unittest.TestCase):
'''
Make sure math renders properly.
'''
def check_parse(self, latex_in, mathjax_out):
xml_str = """<math>{tex}</math>""".format(tex=latex_in)
element = etree.fromstring(xml_str)
renderer = lookup_tag('math')(test_system, element)
self.assertEqual(renderer.mathstr, mathjax_out)
def test_parsing(self):
self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
self.check_parse('$abc', '$abc')
self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
...@@ -8,8 +8,14 @@ Hello</p></text> ...@@ -8,8 +8,14 @@ Hello</p></text>
<text>Click on the image where the top skier will stop momentarily if the top skier starts from rest.</text> <text>Click on the image where the top skier will stop momentarily if the top skier starts from rest.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(242,202)-(296,276)"/> <imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(242,202)-(296,276)"/>
<text>Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.</text> <text>Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<hintgroup showoncorrect="no"> <hintgroup showoncorrect="no">
<text><p>Use conservation of energy.</p></text> <text><p>Use conservation of energy.</p></text>
</hintgroup> </hintgroup>
</imageresponse> </imageresponse>
</problem> </problem>
\ No newline at end of file
""" """
Tests of input types (and actually responsetypes too) Tests of input types.
TODO:
- test unicode in values, parameters, etc.
- test various html escapes
- test funny xml chars -- should never get xml parse error if things are escaped properly.
""" """
from datetime import datetime from lxml import etree
import json
from mock import Mock
from nose.plugins.skip import SkipTest
import os
import unittest import unittest
import xml.sax.saxutils as saxutils
from . import test_system from . import test_system
from capa import inputtypes from capa import inputtypes
from lxml import etree # just a handy shortcut
lookup_tag = inputtypes.registry.get_class_for_tag
def tst_render_template(template, context):
"""
A test version of render to template. Renders to the repr of the context, completely ignoring the template name.
"""
return repr(context)
system = Mock(render_template=tst_render_template) def quote_attr(s):
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
class OptionInputTest(unittest.TestCase): class OptionInputTest(unittest.TestCase):
''' '''
Make sure option inputs work Make sure option inputs work
''' '''
def test_rendering_new(self):
xml = """<optioninput options="('Up','Down')" id="sky_input" correct="Up"/>"""
element = etree.fromstring(xml)
value = 'Down' def test_rendering(self):
status = 'answered' xml_str = """<optioninput options="('Up','Down')" id="sky_input" correct="Up"/>"""
context = inputtypes._optioninput(element, value, status, test_system.render_template) element = etree.fromstring(xml_str)
print 'context: ', context
state = {'value': 'Down',
'id': 'sky_input',
'status': 'answered'}
option_input = lookup_tag('optioninput')(test_system, element, state)
context = option_input._get_render_context()
expected = {'value': 'Down', expected = {'value': 'Down',
'options': [('Up', 'Up'), ('Down', 'Down')], 'options': [('Up', 'Up'), ('Down', 'Down')],
'state': 'answered', 'status': 'answered',
'msg': '', 'msg': '',
'inline': '', 'inline': '',
'id': 'sky_input'} 'id': 'sky_input'}
self.assertEqual(context, expected) self.assertEqual(context, expected)
class ChoiceGroupTest(unittest.TestCase):
'''
Test choice groups, radio groups, and checkbox groups
'''
def check_group(self, tag, expected_input_type, expected_suffix):
xml_str = """
<{tag}>
<choice correct="false" name="foil1"><text>This is foil One.</text></choice>
<choice correct="false" name="foil2"><text>This is foil Two.</text></choice>
<choice correct="true" name="foil3">This is foil Three.</choice>
</{tag}>
""".format(tag=tag)
def test_rendering(self):
xml_str = """<optioninput options="('Up','Down')" id="sky_input" correct="Up"/>"""
element = etree.fromstring(xml_str) element = etree.fromstring(xml_str)
state = {'value': 'Down', state = {'value': 'foil3',
'id': 'sky_input', 'id': 'sky_input',
'status': 'answered'} 'status': 'answered'}
option_input = inputtypes.OptionInput(system, element, state)
context = option_input._get_render_context() the_input = lookup_tag(tag)(test_system, element, state)
expected = {'value': 'Down', context = the_input._get_render_context()
'options': [('Up', 'Up'), ('Down', 'Down')],
'state': 'answered', expected = {'id': 'sky_input',
'value': 'foil3',
'status': 'answered',
'input_type': expected_input_type,
'choices': [('foil1', '<text>This is foil One.</text>'),
('foil2', '<text>This is foil Two.</text>'),
('foil3', 'This is foil Three.'),],
'name_array_suffix': expected_suffix, # what is this for??
}
self.assertEqual(context, expected)
def test_choicegroup(self):
self.check_group('choicegroup', 'radio', '')
def test_radiogroup(self):
self.check_group('radiogroup', 'radio', '[]')
def test_checkboxgroup(self):
self.check_group('checkboxgroup', 'checkbox', '[]')
class JavascriptInputTest(unittest.TestCase):
'''
The javascript input is a pretty straightforward pass-thru, but test it anyway
'''
def test_rendering(self):
params = "(1,2,3)"
problem_state = "abc12',12&hi<there>"
display_class = "a_class"
display_file = "my_files/hi.js"
xml_str = """<javascriptinput id="prob_1_2" params="{params}" problem_state="{ps}"
display_class="{dc}" display_file="{df}"/>""".format(
params=params,
ps=quote_attr(problem_state),
dc=display_class, df=display_file)
element = etree.fromstring(xml_str)
state = {'value': '3',}
the_input = lookup_tag('javascriptinput')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'params': params,
'display_file': display_file,
'display_class': display_class,
'problem_state': problem_state,
'value': '3',
'evaluation': '',}
self.assertEqual(context, expected)
class TextLineTest(unittest.TestCase):
'''
Check that textline inputs work, with and without math.
'''
def test_rendering(self):
size = "42"
xml_str = """<textline id="prob_1_2" size="{size}"/>""".format(size=size)
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee',}
the_input = lookup_tag('textline')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'BumbleBee',
'status': 'unanswered',
'size': size,
'msg': '', 'msg': '',
'inline': '', 'hidden': False,
'id': 'sky_input'} 'inline': False,
'do_math': False,
'preprocessor': None}
self.assertEqual(context, expected)
def test_math_rendering(self):
size = "42"
preprocessorClass = "preParty"
script = "foo/party.js"
xml_str = """<textline math="True" id="prob_1_2" size="{size}"
preprocessorClassName="{pp}"
preprocessorSrc="{sc}"/>""".format(size=size, pp=preprocessorClass, sc=script)
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee',}
the_input = lookup_tag('textline')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'BumbleBee',
'status': 'unanswered',
'size': size,
'msg': '',
'hidden': False,
'inline': False,
'do_math': True,
'preprocessor': {'class_name': preprocessorClass,
'script_src': script}}
self.assertEqual(context, expected)
class FileSubmissionTest(unittest.TestCase):
'''
Check that file submission inputs work
'''
def test_rendering(self):
allowed_files = "runme.py nooooo.rb ohai.java"
required_files = "cookies.py"
xml_str = """<filesubmission id="prob_1_2"
allowed_files="{af}"
required_files="{rf}"
/>""".format(af=allowed_files,
rf=required_files,)
element = etree.fromstring(xml_str)
escapedict = {'"': '&quot;'}
esc = lambda s: saxutils.escape(s, escapedict)
state = {'value': 'BumbleBee.py',
'status': 'incomplete',
'feedback' : {'message': '3'}, }
input_class = lookup_tag('filesubmission')
the_input = input_class(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'status': 'queued',
'msg': input_class.submitted_msg,
'value': 'BumbleBee.py',
'queue_len': '3',
'allowed_files': esc('["runme.py", "nooooo.rb", "ohai.java"]'),
'required_files': esc('["cookies.py"]')}
self.assertEqual(context, expected)
class CodeInputTest(unittest.TestCase):
'''
Check that codeinput inputs work
'''
def test_rendering(self):
mode = "parrot"
linenumbers = 'false'
rows = '37'
cols = '11'
tabsize = '7'
xml_str = """<codeinput id="prob_1_2"
mode="{m}"
cols="{c}"
rows="{r}"
linenumbers="{ln}"
tabsize="{ts}"
/>""".format(m=mode, c=cols, r=rows, ln=linenumbers, ts=tabsize)
element = etree.fromstring(xml_str)
escapedict = {'"': '&quot;'}
esc = lambda s: saxutils.escape(s, escapedict)
state = {'value': 'print "good evening"',
'status': 'incomplete',
'feedback' : {'message': '3'}, }
the_input = lookup_tag('codeinput')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
'msg': 'Submitted to grader.',
'mode': mode,
'linenumbers': linenumbers,
'rows': rows,
'cols': cols,
'hidden': '',
'tabsize': int(tabsize),
'queue_len': '3',
}
self.assertEqual(context, expected)
class SchematicTest(unittest.TestCase):
'''
Check that schematic inputs work
'''
def test_rendering(self):
height = '12'
width = '33'
parts = 'resistors, capacitors, and flowers'
analyses = 'fast, slow, and pink'
initial_value = 'two large batteries'
submit_analyses = 'maybe'
xml_str = """<schematic id="prob_1_2"
height="{h}"
width="{w}"
parts="{p}"
analyses="{a}"
initial_value="{iv}"
submit_analyses="{sa}"
/>""".format(h=height, w=width, p=parts, a=analyses,
iv=initial_value, sa=submit_analyses)
element = etree.fromstring(xml_str)
value = 'three resistors and an oscilating pendulum'
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('schematic')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': value,
'initial_value': initial_value,
'status': 'unsubmitted',
'width': width,
'height': height,
'parts': parts,
'analyses': analyses,
'submit_analyses': submit_analyses,
}
self.assertEqual(context, expected)
class ImageInputTest(unittest.TestCase):
'''
Check that image inputs work
'''
def check(self, value, egx, egy):
height = '78'
width = '427'
src = 'http://www.edx.org/cowclicker.jpg'
xml_str = """<imageinput id="prob_1_2"
src="{s}"
height="{h}"
width="{w}"
/>""".format(s=src, h=height, w=width)
element = etree.fromstring(xml_str)
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('imageinput')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': value,
'status': 'unsubmitted',
'width': width,
'height': height,
'src': src,
'gx': egx,
'gy': egy,
'msg': ''}
self.assertEqual(context, expected)
def test_with_value(self):
# Check that compensating for the dot size works properly.
self.check('[50,40]', 35, 25)
def test_without_value(self):
self.check('', 0, 0)
def test_corrupt_values(self):
self.check('[12', 0, 0)
self.check('[12, a]', 0, 0)
self.check('[12 10]', 0, 0)
self.check('[12]', 0, 0)
self.check('[12 13 14]', 0, 0)
class CrystallographyTest(unittest.TestCase):
'''
Check that crystallography inputs work
'''
def test_rendering(self):
height = '12'
width = '33'
size = '10'
xml_str = """<crystallography id="prob_1_2"
height="{h}"
width="{w}"
size="{s}"
/>""".format(h=height, w=width, s=size)
element = etree.fromstring(xml_str)
value = 'abc'
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('crystallography')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': value,
'status': 'unsubmitted',
'size': size,
'msg': '',
'hidden': '',
'width': width,
'height': height,
}
self.assertEqual(context, expected)
class VseprTest(unittest.TestCase):
'''
Check that vsepr inputs work
'''
def test_rendering(self):
height = '12'
width = '33'
molecules = "H2O, C2O"
geometries = "AX12,TK421"
xml_str = """<vsepr id="prob_1_2"
height="{h}"
width="{w}"
molecules="{m}"
geometries="{g}"
/>""".format(h=height, w=width, m=molecules, g=geometries)
element = etree.fromstring(xml_str)
value = 'abc'
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('vsepr_input')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': value,
'status': 'unsubmitted',
'msg': '',
'width': width,
'height': height,
'molecules': molecules,
'geometries': geometries,
}
self.assertEqual(context, expected)
class ChemicalEquationTest(unittest.TestCase):
'''
Check that chemical equation inputs work.
'''
def test_rendering(self):
size = "42"
xml_str = """<chemicalequationinput id="prob_1_2" size="{size}"/>""".format(size=size)
element = etree.fromstring(xml_str)
state = {'value': 'H2OYeah',}
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'H2OYeah',
'status': 'unanswered',
'size': size,
'previewer': '/static/js/capa/chemical_equation_preview.js',
}
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -53,12 +53,22 @@ class ImageResponseTest(unittest.TestCase): ...@@ -53,12 +53,22 @@ class ImageResponseTest(unittest.TestCase):
imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml" imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml"
test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system) test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system)
correct_answers = {'1_2_1': '(490,11)-(556,98)', correct_answers = {'1_2_1': '(490,11)-(556,98)',
'1_2_2': '(242,202)-(296,276)'} '1_2_2': '(242,202)-(296,276)',
'1_2_3': '(490,11)-(556,98);(242,202)-(296,276)',
'1_2_4': '(490,11)-(556,98);(242,202)-(296,276)',
'1_2_5': '(490,11)-(556,98);(242,202)-(296,276)',
}
test_answers = {'1_2_1': '[500,20]', test_answers = {'1_2_1': '[500,20]',
'1_2_2': '[250,300]', '1_2_2': '[250,300]',
'1_2_3': '[500,20]',
'1_2_4': '[250,250]',
'1_2_5': '[10,10]',
} }
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect') self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_3'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_4'), 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect')
class SymbolicResponseTest(unittest.TestCase): class SymbolicResponseTest(unittest.TestCase):
......
...@@ -26,7 +26,7 @@ setup( ...@@ -26,7 +26,7 @@ setup(
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor", "image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"error = xmodule.error_module:ErrorDescriptor", "error = xmodule.error_module:ErrorDescriptor",
"problem = xmodule.capa_module:CapaDescriptor", "problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.vertical_module:VerticalDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor",
"section = xmodule.backcompat_module:SemanticSectionDescriptor", "section = xmodule.backcompat_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.backcompat_module:TranslateCustomTagDescriptor", "slides = xmodule.backcompat_module:TranslateCustomTagDescriptor",
......
...@@ -119,6 +119,8 @@ class CapaModule(XModule): ...@@ -119,6 +119,8 @@ class CapaModule(XModule):
self.show_answer = self.metadata.get('showanswer', 'closed') self.show_answer = self.metadata.get('showanswer', 'closed')
self.force_save_button = self.metadata.get('force_save_button', 'false')
if self.show_answer == "": if self.show_answer == "":
self.show_answer = "closed" self.show_answer = "closed"
...@@ -319,9 +321,10 @@ class CapaModule(XModule): ...@@ -319,9 +321,10 @@ class CapaModule(XModule):
if not self.lcp.done: if not self.lcp.done:
reset_button = False reset_button = False
# We don't need a "save" button if infinite number of attempts and # We may not need a "save" button if infinite number of attempts and
# non-randomized # non-randomized. The problem author can force it. It's a bit weird for
if self.max_attempts is None and self.rerandomize != "always": # randomization to control this; should perhaps be cleaned up.
if (self.force_save_button == "false") and (self.max_attempts is None and self.rerandomize != "always"):
save_button = False save_button = False
context = {'problem': content, context = {'problem': content,
...@@ -526,15 +529,9 @@ class CapaModule(XModule): ...@@ -526,15 +529,9 @@ class CapaModule(XModule):
lcp_id = self.lcp.problem_id lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst: except StudentInputError as inst:
# TODO (vshnayder): why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
log.exception("StudentInputError in capa_module:problem_check") log.exception("StudentInputError in capa_module:problem_check")
return {'success': inst.message} return {'success': inst.message}
except Exception, err: except Exception, err:
# TODO: why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
if self.system.DEBUG: if self.system.DEBUG:
msg = "Error checking problem: " + str(err) msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc() msg += '\nTraceback:\n' + traceback.format_exc()
......
...@@ -30,13 +30,13 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -30,13 +30,13 @@ class CourseDescriptor(SequenceDescriptor):
self.book_url = book_url self.book_url = book_url
self.table_of_contents = self._get_toc_from_s3() self.table_of_contents = self._get_toc_from_s3()
self.start_page = int(self.table_of_contents[0].attrib['page']) self.start_page = int(self.table_of_contents[0].attrib['page'])
# The last page should be the last element in the table of contents, # The last page should be the last element in the table of contents,
# but it may be nested. So recurse all the way down the last element # but it may be nested. So recurse all the way down the last element
last_el = self.table_of_contents[-1] last_el = self.table_of_contents[-1]
while last_el.getchildren(): while last_el.getchildren():
last_el = last_el[-1] last_el = last_el[-1]
self.end_page = int(last_el.attrib['page']) self.end_page = int(last_el.attrib['page'])
@property @property
...@@ -94,6 +94,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -94,6 +94,7 @@ class CourseDescriptor(SequenceDescriptor):
self.enrollment_start = self._try_parse_time("enrollment_start") self.enrollment_start = self._try_parse_time("enrollment_start")
self.enrollment_end = self._try_parse_time("enrollment_end") self.enrollment_end = self._try_parse_time("enrollment_end")
self.end = self._try_parse_time("end")
# NOTE: relies on the modulestore to call set_grading_policy() right after # NOTE: relies on the modulestore to call set_grading_policy() right after
# init. (Modulestore is in charge of figuring out where to load the policy from) # init. (Modulestore is in charge of figuring out where to load the policy from)
...@@ -237,6 +238,16 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -237,6 +238,16 @@ class CourseDescriptor(SequenceDescriptor):
return definition return definition
def has_ended(self):
"""
Returns True if the current time is after the specified course end date.
Returns False if there is no end date specified.
"""
if self.end_date is None:
return False
return time.gmtime() > self.end
def has_started(self): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
...@@ -346,7 +357,8 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -346,7 +357,8 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def start_date_text(self): def start_date_text(self):
return time.strftime("%b %d, %Y", self.start) displayed_start = self._try_parse_time('advertised_start') or self.start
return time.strftime("%b %d, %Y", displayed_start)
# An extra property is used rather than the wiki_slug/number because # An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows # there are courses that change the number for different runs. This allows
...@@ -375,6 +387,21 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -375,6 +387,21 @@ class CourseDescriptor(SequenceDescriptor):
return self.metadata.get('discussion_link', None) return self.metadata.get('discussion_link', None)
@property @property
def forum_posts_allowed(self):
try:
blackout_periods = [(parse_time(start), parse_time(end))
for start, end
in self.metadata.get('discussion_blackouts', [])]
now = time.gmtime()
for start, end in blackout_periods:
if start <= now <= end:
return False
except:
log.exception("Error parsing discussion_blackouts for course {0}".format(self.id))
return True
@property
def hide_progress_tab(self): def hide_progress_tab(self):
"""TODO: same as above, intended to let internal CS50 hide the progress tab """TODO: same as above, intended to let internal CS50 hide the progress tab
until we get grade integration set up.""" until we get grade integration set up."""
......
...@@ -359,6 +359,34 @@ div.video { ...@@ -359,6 +359,34 @@ div.video {
} }
} }
a.quality_control {
background: url(../images/hd.png) center no-repeat;
border-right: 1px solid #000;
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
color: #797979;
display: block;
float: left;
line-height: 46px; //height of play pause buttons
margin-left: 0;
padding: 0 lh(.5);
text-indent: -9999px;
@include transition();
width: 30px;
&:hover {
background-color: #444;
color: #fff;
text-decoration: none;
}
&.active {
background-color: #F44;
color: #0ff;
text-decoration: none;
}
}
a.hide-subtitles { a.hide-subtitles {
background: url('../images/cc.png') center no-repeat; background: url('../images/cc.png') center no-repeat;
color: #797979; color: #797979;
......
...@@ -216,7 +216,9 @@ class @Problem ...@@ -216,7 +216,9 @@ class @Problem
for choice in value for choice in value
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
else else
@$("#answer_#{key}, #solution_#{key}").html(value) answer = @$("#answer_#{key}, #solution_#{key}")
answer.html(value)
Collapsible.setCollapsibles(answer)
# TODO remove the above once everything is extracted into its own # TODO remove the above once everything is extracted into its own
# inputtype functions. # inputtype functions.
......
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
function image_input_click(id,event){ function image_input_click(id,event){
iidiv = document.getElementById("imageinput_"+id); iidiv = document.getElementById("imageinput_"+id);
pos_x = event.offsetX?(event.offsetX):event.pageX-document.iidiv.offsetLeft; pos_x = event.offsetX?(event.offsetX):event.pageX-iidiv.offsetLeft;
pos_y = event.offsetY?(event.offsetY):event.pageY-document.iidiv.offsetTop; pos_y = event.offsetY?(event.offsetY):event.pageY-iidiv.offsetTop;
result = "[" + pos_x + "," + pos_y + "]"; result = "[" + pos_x + "," + pos_y + "]";
cx = (pos_x-15) +"px"; cx = (pos_x-15) +"px";
cy = (pos_y-15) +"px" ; cy = (pos_y-15) +"px" ;
......
...@@ -22,7 +22,7 @@ class @VideoCaption extends Subview ...@@ -22,7 +22,7 @@ class @VideoCaption extends Subview
""" """
@$('.video-controls .secondary-controls').append """ @$('.video-controls .secondary-controls').append """
<a href="#" class="hide-subtitles" title="Turn off captions">Captions</a> <a href="#" class="hide-subtitles" title="Turn off captions">Captions</a>
""" """#"
@$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5 @$('.subtitles').css maxHeight: @$('.video-wrapper').height() - 5
@fetchCaption() @fetchCaption()
...@@ -144,7 +144,7 @@ class @VideoCaption extends Subview ...@@ -144,7 +144,7 @@ class @VideoCaption extends Subview
@el.removeClass('closed') @el.removeClass('closed')
@scrollCaption() @scrollCaption()
$.cookie('hide_captions', hide_captions, expires: 3650, path: '/') $.cookie('hide_captions', hide_captions, expires: 3650, path: '/')
captionHeight: -> captionHeight: ->
if @el.hasClass('fullscreen') if @el.hasClass('fullscreen')
$(window).height() - @$('.video-controls').height() $(window).height() - @$('.video-controls').height()
......
...@@ -16,7 +16,7 @@ class @VideoControl extends Subview ...@@ -16,7 +16,7 @@ class @VideoControl extends Subview
<a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a> <a href="#" class="add-fullscreen" title="Fill browser">Fill Browser</a>
</div> </div>
</div> </div>
""" """#"
unless onTouchBasedDevice() unless onTouchBasedDevice()
@$('.video_control').addClass('play').html('Play') @$('.video_control').addClass('play').html('Play')
......
...@@ -9,6 +9,7 @@ class @VideoPlayer extends Subview ...@@ -9,6 +9,7 @@ class @VideoPlayer extends Subview
bind: -> bind: ->
$(@control).bind('play', @play) $(@control).bind('play', @play)
.bind('pause', @pause) .bind('pause', @pause)
$(@qualityControl).bind('changeQuality', @handlePlaybackQualityChange)
$(@caption).bind('seek', @onSeek) $(@caption).bind('seek', @onSeek)
$(@speedControl).bind('speedChange', @onSpeedChange) $(@speedControl).bind('speedChange', @onSpeedChange)
$(@progressSlider).bind('seek', @onSeek) $(@progressSlider).bind('seek', @onSeek)
...@@ -25,6 +26,7 @@ class @VideoPlayer extends Subview ...@@ -25,6 +26,7 @@ class @VideoPlayer extends Subview
render: -> render: ->
@control = new VideoControl el: @$('.video-controls') @control = new VideoControl el: @$('.video-controls')
@qualityControl = new VideoQualityControl el: @$('.secondary-controls')
@caption = new VideoCaption @caption = new VideoCaption
el: @el el: @el
youtubeId: @video.youtubeId('1.0') youtubeId: @video.youtubeId('1.0')
...@@ -41,10 +43,12 @@ class @VideoPlayer extends Subview ...@@ -41,10 +43,12 @@ class @VideoPlayer extends Subview
rel: 0 rel: 0
showinfo: 0 showinfo: 0
enablejsapi: 1 enablejsapi: 1
modestbranding: 1
videoId: @video.youtubeId() videoId: @video.youtubeId()
events: events:
onReady: @onReady onReady: @onReady
onStateChange: @onStateChange onStateChange: @onStateChange
onPlaybackQualityChange: @onPlaybackQualityChange
@caption.hideCaptions(@['video'].hide_captions) @caption.hideCaptions(@['video'].hide_captions)
addToolTip: -> addToolTip: ->
...@@ -53,7 +57,7 @@ class @VideoPlayer extends Subview ...@@ -53,7 +57,7 @@ class @VideoPlayer extends Subview
my: 'top right' my: 'top right'
at: 'top center' at: 'top center'
onReady: => onReady: (event) =>
unless onTouchBasedDevice() unless onTouchBasedDevice()
$('.video-load-complete:first').data('video').player.play() $('.video-load-complete:first').data('video').player.play()
...@@ -68,6 +72,13 @@ class @VideoPlayer extends Subview ...@@ -68,6 +72,13 @@ class @VideoPlayer extends Subview
when YT.PlayerState.ENDED when YT.PlayerState.ENDED
@onEnded() @onEnded()
onPlaybackQualityChange: (event, value) =>
quality = @player.getPlaybackQuality()
@qualityControl.onQualityChange(quality)
handlePlaybackQualityChange: (event, value) =>
@player.setPlaybackQuality(value)
onUnstarted: => onUnstarted: =>
@control.pause() @control.pause()
@caption.pause() @caption.pause()
......
class @VideoQualityControl extends Subview
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
...@@ -17,7 +17,7 @@ class @VideoVolumeControl extends Subview ...@@ -17,7 +17,7 @@ class @VideoVolumeControl extends Subview
<div class="volume-slider"></div> <div class="volume-slider"></div>
</div> </div>
</div> </div>
""" """#"
@slider = @$('.volume-slider').slider @slider = @$('.volume-slider').slider
orientation: "vertical" orientation: "vertical"
range: "min" range: "min"
......
...@@ -36,7 +36,7 @@ metadata: ...@@ -36,7 +36,7 @@ metadata:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Example "multiple choice" problem} \subsection{Example "multiple choice" problem}
What color is a bannana? What color is a banana?
\edXabox{ type="multichoice" expect="Yellow" options="Red","Green","Yellow","Blue" } \edXabox{ type="multichoice" expect="Yellow" options="Red","Green","Yellow","Blue" }
...@@ -129,7 +129,7 @@ data: | ...@@ -129,7 +129,7 @@ data: |
<h4>Example "multiple choice" problem</h4> <h4>Example "multiple choice" problem</h4>
</p> </p>
<p> <p>
What color is a bannana? </p> What color is a banana? </p>
<p> <p>
<choiceresponse> <choiceresponse>
<checkboxgroup> <checkboxgroup>
......
...@@ -190,7 +190,7 @@ case `uname -s` in ...@@ -190,7 +190,7 @@ case `uname -s` in
} }
distro=`lsb_release -cs` distro=`lsb_release -cs`
case $distro in case $distro in
maya|lisa|natty|oneiric|precise) maya|lisa|natty|oneiric|precise|quantal)
output "Installing ubuntu requirements" output "Installing ubuntu requirements"
sudo apt-get -y update sudo apt-get -y update
sudo apt-get -y install $APT_PKGS sudo apt-get -y install $APT_PKGS
......
...@@ -250,8 +250,11 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}. ...@@ -250,8 +250,11 @@ Values are dictionaries of the form {"metadata-key" : "metadata-value"}.
Supported fields at the course level: Supported fields at the course level:
* "start" -- specify the start date for the course. Format-by-example: "2012-09-05T12:00". * "start" -- specify the start date for the course. Format-by-example: "2012-09-05T12:00".
* "advertised_start" -- specify what you want displayed as the start date of the course in the course listing and course about pages. This can be useful if you want to let people in early before the formal start. Format-by-example: "2012-09-05T12:00".
* "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start". * "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start".
* "end" -- specify the end date for the course. Format-by-example: "2012-11-05T12:00".
* "tabs" -- have custom tabs in the courseware. See below for details on config. * "tabs" -- have custom tabs in the courseware. See below for details on config.
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
* TODO: there are others * TODO: there are others
### Grading policy file contents ### Grading policy file contents
...@@ -308,7 +311,7 @@ __Inherited:__ ...@@ -308,7 +311,7 @@ __Inherited:__
* `start` -- when this content should be shown to students. Note that anyone with staff access to the course will always see everything. * `start` -- when this content should be shown to students. Note that anyone with staff access to the course will always see everything.
* `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional. * `showanswer` - When to show answer. For 'attempted', will show answer after first attempt. Values: never, attempted, answered, closed. Default: closed. Optional.
* `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false". * `graded` - Whether this section will count towards the students grade. "true" or "false". Defaults to "false".
* `rerandomise` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it) * `rerandomize` - Randomize question on each attempt. Values: 'always' (students see a different version of the problem after each attempt to solve it)
'onreset' (randomize question when reset button is pressed by the student) 'onreset' (randomize question when reset button is pressed by the student)
'never' (all students see the same version of the problem) 'never' (all students see the same version of the problem)
'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see) 'per_student' (individual students see the same version of the problem each time the look at it, but that version is different from what other students see)
......
Instructions
============
For each pull request, add one or more lines to the bottom of the change list. When
code is released to production, change the `Upcoming` entry to todays date, and add
a new block at the bottom of the file.
Upcoming
--------
Change log entries should be targeted at end users. A good place to start is the
user story that instigated the pull request.
Changes
=======
Upcoming
--------
* Created changelog
\ No newline at end of file
...@@ -29,6 +29,7 @@ from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor ...@@ -29,6 +29,7 @@ from xmodule.error_module import ErrorDescriptor, NonStaffErrorDescriptor
from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule from xmodule_modifiers import replace_course_urls, replace_static_urls, add_histogram, wrap_xmodule
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from statsd import statsd
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -342,7 +343,7 @@ def xqueue_callback(request, course_id, userid, id, dispatch): ...@@ -342,7 +343,7 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
''' '''
# Test xqueue package, which we expect to be: # Test xqueue package, which we expect to be:
# xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}), # xpackage = {'xqueue_header': json.dumps({'lms_key':'secretkey',...}),
# 'xqueue_body' : 'Message from grader} # 'xqueue_body' : 'Message from grader'}
get = request.POST.copy() get = request.POST.copy()
for key in ['xqueue_header', 'xqueue_body']: for key in ['xqueue_header', 'xqueue_body']:
if not get.has_key(key): if not get.has_key(key):
...@@ -377,7 +378,8 @@ def xqueue_callback(request, course_id, userid, id, dispatch): ...@@ -377,7 +378,8 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
# We go through the "AJAX" path # We go through the "AJAX" path
# So far, the only dispatch from xqueue will be 'score_update' # So far, the only dispatch from xqueue will be 'score_update'
try: try:
ajax_return = instance.handle_ajax(dispatch, get) # Can ignore the "ajax" return in 'xqueue_callback' # Can ignore the return value--not used for xqueue_callback
instance.handle_ajax(dispatch, get)
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
...@@ -389,6 +391,15 @@ def xqueue_callback(request, course_id, userid, id, dispatch): ...@@ -389,6 +391,15 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
if instance_module.grade != oldgrade or instance_module.state != old_instance_state: if instance_module.grade != oldgrade or instance_module.state != old_instance_state:
instance_module.save() instance_module.save()
#Bin score into range and increment stats
score_bucket=get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run=course_id.split("/")
statsd.increment("lms.courseware.question_answered",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run),
"score_bucket:{0}".format(score_bucket),
"type:xqueue"])
return HttpResponse("") return HttpResponse("")
...@@ -473,6 +484,17 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -473,6 +484,17 @@ def modx_dispatch(request, dispatch, location, course_id):
instance_module.max_grade != old_instance_max_grade): instance_module.max_grade != old_instance_max_grade):
instance_module.save() instance_module.save()
#Bin score into range and increment stats
score_bucket=get_score_bucket(instance_module.grade, instance_module.max_grade)
org, course_num, run=course_id.split("/")
statsd.increment("lms.courseware.question_answered",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run),
"score_bucket:{0}".format(score_bucket),
"type:ajax"])
if shared_module is not None: if shared_module is not None:
shared_module.state = instance.get_shared_state() shared_module.state = instance.get_shared_state()
if shared_module.state != old_shared_state: if shared_module.state != old_shared_state:
...@@ -518,4 +540,17 @@ def preview_chemcalc(request): ...@@ -518,4 +540,17 @@ def preview_chemcalc(request):
return HttpResponse(json.dumps(result)) return HttpResponse(json.dumps(result))
def get_score_bucket(grade,max_grade):
"""
Function to split arbitrary score ranges into 3 buckets.
Used with statsd tracking.
"""
score_bucket="incorrect"
if(grade>0 and grade<max_grade):
score_bucket="partial"
elif(grade==max_grade):
score_bucket="correct"
return score_bucket
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django_comment_client.models import Permission, Role from django_comment_client.models import Role
from django.contrib.auth.models import User from django.contrib.auth.models import User
class Command(BaseCommand): class Command(BaseCommand):
args = 'user role course_id' option_list = BaseCommand.option_list + (
help = 'Assign a role to a user' make_option('--remove',
action='store_true',
dest='remove',
default=False,
help='Remove the role instead of adding it'),
)
args = '<user|email> <role> <course_id>'
help = 'Assign a discussion forum role to a user '
def handle(self, *args, **options): def handle(self, *args, **options):
role = Role.objects.get(name=args[1], course_id=args[2]) if len(args) != 3:
raise CommandError('Usage is assign_role {0}'.format(self.args))
name_or_email, role, course_id = args
role = Role.objects.get(name=role, course_id=course_id)
if '@' in name_or_email:
user = User.objects.get(email=name_or_email)
else:
user = User.objects.get(username=name_or_email)
if '@' in args[0]: if options['remove']:
user = User.objects.get(email=args[0]) user.roles.remove(role)
else: else:
user = User.objects.get(username=args[0]) user.roles.add(role)
user.roles.add(role) print 'Success!'
\ No newline at end of file
...@@ -2,6 +2,7 @@ from django.db import models ...@@ -2,6 +2,7 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
import logging import logging
from courseware.courses import get_course_by_id
class Role(models.Model): class Role(models.Model):
name = models.CharField(max_length=30, null=False, blank=False) name = models.CharField(max_length=30, null=False, blank=False)
...@@ -23,6 +24,12 @@ class Role(models.Model): ...@@ -23,6 +24,12 @@ class Role(models.Model):
self.permissions.add(Permission.objects.get_or_create(name=permission)[0]) self.permissions.add(Permission.objects.get_or_create(name=permission)[0])
def has_permission(self, permission): def has_permission(self, permission):
course = get_course_by_id(self.course_id)
if self.name == "Student" and \
(permission.startswith('edit') or permission.startswith('update') or permission.startswith('create')) and \
(not course.forum_posts_allowed):
return False
return self.permissions.filter(name=permission).exists() return self.permissions.filter(name=permission).exists()
......
...@@ -160,7 +160,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -160,7 +160,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
) )
STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB STUDENT_FILEUPLOAD_MAX_SIZE = 4*1000*1000 # 4 MB
MAX_FILEUPLOADS_PER_INPUT = 10 MAX_FILEUPLOADS_PER_INPUT = 20
# FIXME: # FIXME:
# We should have separate S3 staged URLs in case we need to make changes to # We should have separate S3 staged URLs in case we need to make changes to
......
...@@ -136,6 +136,14 @@ ...@@ -136,6 +136,14 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
h4 {
font-size: 1.0em;
font-family: $sans-serif;
font-weight: 700;
margin-top: 25px;
margin-bottom: 10px;
}
ul { ul {
padding-left: 50px; padding-left: 50px;
} }
......
<%! from django_comment_client.permissions import has_permission %>
<%inherit file="../courseware/course_navigation.html" /> <%inherit file="../courseware/course_navigation.html" />
<%block name="extratabs"> <%block name="extratabs">
% if has_permission(user, 'create_thread', course.id):
<li class="right"><a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a></li> <li class="right"><a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a></li>
% endif
</%block> </%block>
\ No newline at end of file
...@@ -3,4 +3,6 @@ ...@@ -3,4 +3,6 @@
<div class="discussion-module" data-discussion-id="${discussion_id | h}"> <div class="discussion-module" data-discussion-id="${discussion_id | h}">
<a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}"><span class="show-hide-discussion-icon"></span><span class="button-text">Show Discussion</span></a> <a class="discussion-show control-button" href="javascript:void(0)" data-discussion-id="${discussion_id | h}"><span class="show-hide-discussion-icon"></span><span class="button-text">Show Discussion</span></a>
<a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a> <a href="#" class="new-post-btn"><span class="new-post-icon"></span>New Post</a>
</div> </div>
<%! from django_comment_client.permissions import has_permission %>
<script type="text/template" id="thread-template"> <script type="text/template" id="thread-template">
<article class="discussion-article" data-id="${'<%- id %>'}"> <article class="discussion-article" data-id="${'<%- id %>'}">
<div class="thread-content-wrapper"></div> <div class="thread-content-wrapper"></div>
...@@ -7,6 +9,7 @@ ...@@ -7,6 +9,7 @@
<div class="post-status-closed bottom-post-status" style="display: none"> <div class="post-status-closed bottom-post-status" style="display: none">
This thread is closed. This thread is closed.
</div> </div>
% if course is UNDEFINED or has_permission(user, 'create_comment', course.id):
<form class="discussion-reply-new" data-id="${'<%- id %>'}"> <form class="discussion-reply-new" data-id="${'<%- id %>'}">
<h4>Post a response:</h4> <h4>Post a response:</h4>
<ul class="discussion-errors"></ul> <ul class="discussion-errors"></ul>
...@@ -15,6 +18,7 @@ ...@@ -15,6 +18,7 @@
<a class="discussion-submit-post control-button" href="#">Submit</a> <a class="discussion-submit-post control-button" href="#">Submit</a>
</div> </div>
</form> </form>
% endif
</article> </article>
</script> </script>
...@@ -75,6 +79,7 @@ ...@@ -75,6 +79,7 @@
<div class="discussion-response"></div> <div class="discussion-response"></div>
<ol class="comments"> <ol class="comments">
<li class="new-comment response-local"> <li class="new-comment response-local">
% if course is UNDEFINED or has_permission(user, 'create_sub_comment', course.id):
<form class="comment-form" data-id="${'<%- wmdId %>'}"> <form class="comment-form" data-id="${'<%- wmdId %>'}">
<ul class="discussion-errors"></ul> <ul class="discussion-errors"></ul>
<div class="comment-body" data-id="${'<%- wmdId %>'}" <div class="comment-body" data-id="${'<%- wmdId %>'}"
...@@ -83,6 +88,7 @@ ...@@ -83,6 +88,7 @@
<a class="discussion-submit-comment control-button" href="#">Submit</a> <a class="discussion-submit-comment control-button" href="#">Submit</a>
</div> </div>
</form> </form>
% endif
</li> </li>
</ol> </ol>
</script> </script>
......
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-35248639-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
...@@ -21,20 +21,9 @@ ...@@ -21,20 +21,9 @@
<meta name="path_prefix" content="${MITX_ROOT_URL}"> <meta name="path_prefix" content="${MITX_ROOT_URL}">
% if not course: % if not course:
<script type="text/javascript"> <%include file="google_analytics.html" />
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-35248639-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
% endif % endif
</head> </head>
<body class="<%block name='bodyclass'/>"> <body class="<%block name='bodyclass'/>">
......
...@@ -7,7 +7,12 @@ ...@@ -7,7 +7,12 @@
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%block name="headextra">
<%include file="../google_analytics.html" />
</%block>
<%block name="js_extra"> <%block name="js_extra">
% if not registered: % if not registered:
%if user.is_authenticated(): %if user.is_authenticated():
## If the user is authenticated, clicking the enroll button just submits a form ## If the user is authenticated, clicking the enroll button just submits a form
......
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%block name="title"><title>Jobs</title></%block> <%block name="title"><title>Jobs</title></%block>
...@@ -50,12 +49,79 @@ ...@@ -50,12 +49,79 @@
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p> <p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div> </div>
</article> </article>
<article id="learning-designer" class="job">
<div class="inner-wrapper">
<h3>Learning Designer/Interaction Learning Designer </h3>
<p>The Learning Designer will work as part of the content and development team to plan, develop and deliver highly engaging and media rich online courses. The learning designer will be a flexible thinker, able to determine and apply sound pedagogical strategies to unique situations and a diverse set of academic disciplines. This is a 6-12 months contract opportunity.</p>
<h4>Specific Responsibilities include: </h4>
<ul>
<li>Work with producers, product developers and course staff on implementing instructional design approaches in the development of media and other course materials. </li>
<li>Articulate learning objectives and align them to content design strategy and assessments. </li>
<li>Write effective instructional text, and audio and video scripts. </li>
<li>Coordinate workflows with video and content development team</li>
<li>Identify best practices and share these with the course staff and faculty as needed. </li>
<li>Create course communication style guides. Train and coach teaching staff on best practices for communication and discussion management. </li>
<li>Develop use case guides as needed on the use of edX courseware and new technologies. </li>
<li>Serve as a liaison to instructional design teams located at X universities. </li>
<li>Design peer review processes to be used by learners in selected courses. </li>
<li>Ability to apply game-based learning theory and design into selected courses as appropriate.</li>
<li>Use learning analytics and metrics to inform course design and revision process. </li>
<li>Work closely with the Content Research Director on articulating best practices for MOOC teaching and learning and course design.</li>
<li>Assist in the development of pilot courses used for sponsored research initiatives. </li>
</ul>
<h4>Qualifications:</h4>
<p>Master's Degree in Educational Technology, Instructional Design or related field. Experience in higher education with additional experience in a start-up or research environment desirable. Excellent interpersonal and communication (written and verbal), project management, problem-solving and time management skills. The ability to be flexible with projects and to work on multiple courses essential. Ability to meet deadlines and manage expectations of constituents. Capacity to develop new and relevant technology skills. &nbsp;Experience using game theory design and learning analytics to inform instructional design decisions and strategy.</p>
<h4>Technical Skills:</h4>
<p>Video and screencasting experience. LMS Platform experience, xml, HTML, CSS, Adobe Design Suite, Camtasia or Captivate experience. Experience with web 2.0 collaboration tools.</p>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div>
</article>
<article id="production-coordinator" class="job">
<div class="inner-wrapper">
<h3>Production Coordinator</h3>
<p>The Production Coordinator supports video editors and course staff in all video related tasks, such as ingesting footage, transcoding, tracking live dates, transcriptions, organizing project deliverables and archiving completed projects.</p>
<h4>Primary responsibilities:</h4>
<ul>
<li>organize, track, and manage video and associated assets across the video workflow</li>
<li>manage project data and spreadsheets</li>
<li>route incoming source footage, and apply metadata tags</li>
<li>run encoding/transcoding jobs </li>
<li>prepare and process associated video assets, such as slides and image files</li>
<li>manage the transcription process </li>
<ul type="circle">
<li>traffic files among project staff and video transcription services</li>
<li>coordinate transcript reviews with course staff</li>
<li>integrate transcripts in course pages</li>
</ul>
<li>other video-related tasks as assigned.</li>
</ul>
<br/>
<h4>Qualifications</h4>
<p>The ideal candidate for the Production Coordinator position will have</p>
<ul>
<li>relentless attention to detail</li>
<li>ability to communicate and collaborate effectively across the organization</li>
<li>knowledge and understanding of digital media production tools and processes</li>
<li>experience with compression techniques, image processing, and presentation software preferred</li>
<li>proficiency with standard office applications </li>
<ul type="circle">
<li>spreadsheets</li>
<li>word processing</li>
<li>presentation</li>
</ul>
<li>experience with web publishing, e.g., HTML, XML, CSS, a plus</li>
</ul>
<p>If you are interested in this position, please send an email to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
</div>
</article>
</section> </section>
<section class="jobs-sidebar"> <section class="jobs-sidebar">
<h2>Positions</h2> <h2>Positions</h2>
<nav> <nav>
<a href="#content-engineer">EdX Content Engineer</a> <a href="#content-engineer">EdX Content Engineer</a>
<a href="#platform-developer">Platform Developer</a> <a href="#platform-developer">Platform Developer</a>
<a href="#learning-designer">Learning Designer</a>
<a href="#production-coordinator">Production Coordinator</a>
</nav> </nav>
<h2>How to Apply</h2> <h2>How to Apply</h2>
<p>E-mail your resume, coverletter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p> <p>E-mail your resume, coverletter and any other materials to <a href="mailto:jobs@edx.org">jobs@edx.org</a></p>
......
...@@ -50,3 +50,4 @@ pystache==0.3.1 ...@@ -50,3 +50,4 @@ pystache==0.3.1
python-openid==2.2.5 python-openid==2.2.5
South==0.7.5 South==0.7.5
Unidecode==0.04.9 Unidecode==0.04.9
dogstatsd-python==0.2.1
...@@ -50,3 +50,5 @@ pygraphviz ...@@ -50,3 +50,5 @@ pygraphviz
-r repo-requirements.txt -r repo-requirements.txt
pil pil
nltk nltk
dogstatsd-python
MySQL-python
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