Commit 34a696cd by Calen Pennington

Merge branch 'master' into cpennington/cms

Conflicts:
	rakefile
	requirements.txt
parents 79e81d69 40a950f1
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
[MESSAGES CONTROL]
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once).
#disable=
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=text
# Include message's id in output
include-ids=no
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,apply,input
# Regular expression which should only match correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression which should only match correct module level names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression which should only match correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression which should only match functions or classes name which do
# not require a docstring
no-docstring-rgx=__.*__
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the beginning of the name of dummy variables
# (i.e. not used).
dummy-variables-rgx=_|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branchs=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
#-----------------------------------------------------------------------------
# class used to store graded responses to CAPA questions
#
# Used by responsetypes and capa_problem
class CorrectMap(object):
'''
Stores map between answer_id and response evaluation result for each question
in a capa problem. The response evaluation result for each answer_id includes
(correctness, npoints, msg, hint, hintmode).
- correctness : either 'correct' or 'incorrect'
- npoints : None, or integer specifying number of points awarded for this answer_id
- msg : string (may have HTML) giving extra message response (displayed below textline or textbox)
- hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg)
- hintmode : one of (None,'on_request','always') criteria for displaying hint
Behaves as a dict.
'''
def __init__(self,*args,**kwargs):
self.cmap = dict() # start with empty dict
self.items = self.cmap.items
self.keys = self.cmap.keys
self.set(*args,**kwargs)
def __getitem__(self, *args, **kwargs):
return self.cmap.__getitem__(*args, **kwargs)
def __iter__(self):
return self.cmap.__iter__()
def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None):
if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness,
'npoints': npoints,
'msg': msg,
'hint' : hint,
'hintmode' : hintmode,
}
def __repr__(self):
return repr(self.cmap)
def get_dict(self):
'''
return dict version of self
'''
return self.cmap
def set_dict(self,correct_map):
'''
set internal dict to provided correct_map dict
for graceful migration, if correct_map is a one-level dict, then convert it to the new
dict of dicts format.
'''
if correct_map and not (type(correct_map[correct_map.keys()[0]])==dict):
self.__init__() # empty current dict
for k in correct_map: self.set(k,correct_map[k]) # create new dict entries
else:
self.cmap = correct_map
def is_correct(self,answer_id):
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct'
return None
def get_npoints(self,answer_id):
if self.is_correct(answer_id):
npoints = self.cmap[answer_id].get('npoints',1) # default to 1 point if correct
return npoints or 1
return 0 # if not correct, return 0
def set_property(self,answer_id,property,value):
if answer_id in self.cmap: self.cmap[answer_id][property] = value
else: self.cmap[answer_id] = {property:value}
def get_property(self,answer_id,property,default=None):
if answer_id in self.cmap: return self.cmap[answer_id].get(property,default)
return default
def get_correctness(self,answer_id):
return self.get_property(answer_id,'correctness')
def get_msg(self,answer_id):
return self.get_property(answer_id,'msg','')
def get_hint(self,answer_id):
return self.get_property(answer_id,'hint','')
def get_hintmode(self,answer_id):
return self.get_property(answer_id,'hintmode',None)
def set_hint_and_mode(self,answer_id,hint,hintmode):
'''
- hint : (string) HTML text for hint
- hintmode : (string) mode for hint display ('always' or 'on_request')
'''
self.set_property(answer_id,'hint',hint)
self.set_property(answer_id,'hintmode',hintmode)
def update(self,other_cmap):
'''
Update this CorrectMap with the contents of another CorrectMap
'''
if not isinstance(other_cmap,CorrectMap):
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
self.cmap.update(other_cmap.get_dict())
""" Standard resistor codes.
http://en.wikipedia.org/wiki/Electronic_color_code
"""
E6=[10,15,22,33,47,68]
E12=[10,12,15,18,22,27,33,39,47,56,68,82]
E24=[10,12,15,18,22,27,33,39,47,56,68,82,11,13,16,20,24,30,36,43,51,62,75,91]
......
......@@ -32,100 +32,71 @@ def get_input_xml_tags():
return SimpleInput.get_xml_tags()
class SimpleInput():# XModule
''' Type for simple inputs -- plain HTML with a form element
State is a dictionary with optional keys:
* Value
* ID
* Status (answered, unanswered, unsubmitted)
* Feedback (dictionary containing keys for hints, errors, or other
feedback from previous attempt)
'''
Type for simple inputs -- plain HTML with a form element
'''
xml_tags = {} ## Maps tags to functions
@classmethod
def get_xml_tags(c):
return c.xml_tags.keys()
@classmethod
def get_uses(c):
return ['capa_input', 'capa_transform']
def get_html(self):
return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)
def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
'''
Instantiate a SimpleInput class. Arguments:
- system : I4xSystem instance which provides OS, rendering, and user context
- xml : Element tree of this Input element
- item_id : id for this input element (assigned by capa_problem.LoncapProblem) - string
- track_url : URL used for tracking - string
- state : a dictionary with optional keys:
* Value
* ID
* Status (answered, unanswered, unsubmitted)
* Feedback (dictionary containing keys for hints, errors, or other
feedback from previous attempt)
- use :
'''
self.xml = xml
self.tag = xml.tag
if not state:
state = {}
self.system = system
if not state: state = {}
## ID should only come from one place.
## If it comes from multiple, we use state first, XML second, and parameter
## third. Since we don't make this guarantee, we can swap this around in
## the future if there's a more logical order.
if item_id:
self.id = item_id
if xml.get('id'):
self.id = xml.get('id')
if 'id' in state:
self.id = state['id']
self.system = system
if item_id: self.id = item_id
if xml.get('id'): self.id = xml.get('id')
if 'id' in state: self.id = state['id']
self.value = ''
if 'value' in state:
self.value = state['value']
self.msg = ''
if 'feedback' in state and 'message' in state['feedback']:
self.msg = state['feedback']['message']
feedback = state.get('feedback')
if feedback is not None:
self.msg = feedback.get('message','')
self.hint = feedback.get('hint','')
self.hintmode = feedback.get('hintmode',None)
# put hint above msg if to be displayed
if self.hintmode == 'always':
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
self.status = 'unanswered'
if 'status' in state:
self.status = state['status']
## TODO
# class SimpleTransform():
# ''' Type for simple XML to HTML transforms. Examples:
# * Math tags, which go from LON-CAPA-style m-tags to MathJAX
# '''
# xml_tags = {} ## Maps tags to functions
# @classmethod
# def get_xml_tags(c):
# return c.xml_tags.keys()
# @classmethod
# def get_uses(c):
# return ['capa_transform']
# def get_html(self):
# return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg)
# def __init__(self, system, xml, item_id = None, track_url=None, state=None, use = 'capa_input'):
# self.xml = xml
# self.tag = xml.tag
# if not state:
# state = {}
# if item_id:
# self.id = item_id
# if xml.get('id'):
# self.id = xml.get('id')
# if 'id' in state:
# self.id = state['id']
# self.system = system
# self.value = ''
# if 'value' in state:
# self.value = state['value']
# self.msg = ''
# if 'feedback' in state and 'message' in state['feedback']:
# self.msg = state['feedback']['message']
# self.status = 'unanswered'
# if 'status' in state:
# self.status = state['status']
@classmethod
def get_xml_tags(c):
return c.xml_tags.keys()
@classmethod
def get_uses(c):
return ['capa_input', 'capa_transform']
def get_html(self):
return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)
def register_render_function(fn, names=None, cls=SimpleInput):
if names is None:
......@@ -136,9 +107,6 @@ def register_render_function(fn, names=None, cls=SimpleInput):
return fn
return wrapped
#-----------------------------------------------------------------------------
@register_render_function
......@@ -201,16 +169,20 @@ def choicegroup(element, value, status, render_template, msg=''):
return etree.XML(html)
@register_render_function
def textline(element, value, state, render_template, msg=""):
def textline(element, value, status, render_template, msg=""):
'''
Simple text line input, with optional size specification.
'''
if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x
return SimpleInput.xml_tags['textline_dynamath'](element,value,state,msg)
return SimpleInput.xml_tags['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')
context = {'id':eid, 'value':value, 'state':state, 'count':count, 'size': size, 'msg': msg}
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg': msg}
html = render_template("textinput.html", context)
return etree.XML(html)
......
......@@ -2,49 +2,35 @@
### version of textline.html which does dynammic math
###
<section class="text-input-dynamath">
<table style="display:inline; vertical-align:middle;"><tr><td>
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
onkeyup="DoUpdateMath('${id}')"
/>
</td><td>
<table style="display:inline; vertical-align:middle;">
<tr>
<td>
<input type="text" name="input_${id}" id="input_${id}" value="${value}" class="math" size="${size if size else ''}" />
</td>
<td>
<span id="answer_${id}"></span>
<span id="answer_${id}"></span>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
</td></tr><tr><td>
<span id="display_${id}">`{::}`</span>
</td><td>
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> </textarea>
</td></tr>
</table>
##
## javascript for dynamic math: add this math element to the MathJax rendering queue
## also adds to global jaxset js array
##
<script type="text/javascript">
MathJax.Hub.queue.Push(function () {
math = MathJax.Hub.getAllJax("display_${id}")[0];
if (math){
jaxset["${id}"] = math;
math.Text(document.getElementById("input_${id}").value);
// UpdateMathML(math,"${id}");
}
});
</script>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
</td>
</tr>
<tr>
<td>
<span id="display_${id}">`{::}`</span>
</td>
<td>
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> </textarea>
</td>
</tr>
</table>
% if msg:
<br/>
<span class="debug">${msg|n}</span>
<span class="message">${msg|n}</span>
% endif
</section>
from calc import evaluator, UndefinedVariable
#-----------------------------------------------------------------------------
#
# Utility functions used in CAPA responsetypes
def compare_with_tolerance(v1, v2, tol):
''' Compare v1 to v2 with maximum tolerance tol
tol is relative if it ends in %; otherwise, it is absolute
- v1 : student result (number)
- v2 : instructor result (number)
- tol : tolerance (string or number)
'''
relative = tol.endswith('%')
if relative:
tolerance_rel = evaluator(dict(),dict(),tol[:-1]) * 0.01
tolerance = tolerance_rel * max(abs(v1), abs(v2))
else:
tolerance = evaluator(dict(),dict(),tol)
return abs(v1-v2) <= tolerance
def contextualize_text(text, context): # private
''' Takes a string with variables. E.g. $a+$b.
Does a substitution of those variables from the context '''
......
......@@ -13,6 +13,7 @@ from lxml import etree
from x_module import XModule, XModuleDescriptor
from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
......@@ -280,6 +281,7 @@ class Module(XModule):
def answer_available(self):
''' Is the user allowed to see an answer?
TODO: simplify.
'''
if self.show_answer == '':
return False
......@@ -365,18 +367,17 @@ class Module(XModule):
self.attempts = self.attempts + 1
self.lcp.done=True
success = 'correct'
for i in correct_map:
if correct_map[i]!='correct':
success = 'correct' # success = correct if ALL questions in this problem are correct
for answer_id in correct_map:
if not correct_map.is_correct(answer_id):
success = 'incorrect'
event_info['correct_map']=correct_map
event_info['correct_map']=correct_map.get_dict() # log this in the tracker
event_info['success']=success
self.tracker('save_problem_check', event_info)
try:
html = self.get_problem_html(encapsulate=False)
html = self.get_problem_html(encapsulate=False) # render problem into HTML
except Exception,err:
log.error('failed to generate html')
raise Exception,err
......@@ -430,17 +431,10 @@ class Module(XModule):
self.tracker('reset_problem_fail', event_info)
return "Refresh the page and make an attempt before resetting."
self.lcp.done=False
self.lcp.answers=dict()
self.lcp.correct_map=dict()
self.lcp.student_answers = dict()
self.lcp.do_reset() # call method in LoncapaProblem to reset itself
if self.rerandomize == "always":
self.lcp.context=dict()
self.lcp.questions=dict() # Detailed info about questions in problem instance. TODO: Should be by id and not lid.
self.lcp.seed=None
self.lcp.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line)
self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system)
event_info['new_state']=self.lcp.get_state()
......
<problem>
<script type="loncapa/python">
# from loncapa import *
x1 = 4 # lc_random(2,4,1)
y1 = 5 # lc_random(3,7,1)
x2 = 10 # lc_random(x1+1,9,1)
y2 = 20 # lc_random(y1+1,15,1)
m = (y2-y1)/(x2-x1)
b = y1 - m*x1
answer = "%s*x+%s" % (m,b)
answer = answer.replace('+-','-')
inverted_m = (x2-x1)/(y2-y1)
inverted_b = b
wrongans = "%s*x+%s" % (inverted_m,inverted_b)
wrongans = wrongans.replace('+-','-')
</script>
<text>
<p>Hints can be provided to students, based on the last response given, as well as the history of responses given. Here is an example of a hint produced by a Formula Response problem.</p>
<p>
What is the equation of the line which passess through ($x1,$y1) and
($x2,$y2)?</p>
<p>The correct answer is <tt>$answer</tt>. A common error is to invert the equation for the slope. Enter <tt>
$wrongans</tt> to see a hint.</p>
</text>
<formularesponse samples="x@-5:5#11" id="11" answer="$answer">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.001" name="tol" />
<text>y = <textline size="25" /></text>
<hintgroup>
<formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad">
</formulahint>
<hintpart on="inversegrad">
<text>You have inverted the slope in the question.</text>
</hintpart>
</hintgroup>
</formularesponse>
</problem>
<problem >
<text><h2>Example: String Response Problem</h2>
<br/>
</text>
<text>Which US state has Lansing as its capital?</text>
<stringresponse answer="Michigan" type="ci">
<textline size="20" />
<hintgroup>
<stringhint answer="wisconsin" type="cs" name="wisc">
</stringhint>
<stringhint answer="minnesota" type="cs" name="minn">
</stringhint>
<hintpart on="wisc">
<text>The state capital of Wisconsin is Madison.</text>
</hintpart>
<hintpart on="minn">
<text>The state capital of Minnesota is St. Paul.</text>
</hintpart>
<hintpart on="default">
<text>The state you are looking for is also known as the 'Great Lakes State'</text>
</hintpart>
</hintgroup>
</stringresponse>
</problem>
#
# unittests for courseware
# unittests for xmodule (and capa)
#
# Note: run this using a like like this:
#
# django-admin.py test --settings=envs.test_ike --pythonpath=. courseware
# django-admin.py test --settings=lms.envs.test_ike --pythonpath=. common/lib/xmodule
import unittest
import os
......@@ -28,12 +28,13 @@ class I4xSystem(object):
self.track_function = lambda x: None
self.render_function = lambda x: {} # Probably incorrect
self.exception404 = Exception
self.DEBUG = True
def __repr__(self):
return repr(self.__dict__)
def __str__(self):
return str(self.__dict__)
i4xs = I4xSystem
i4xs = I4xSystem()
class ModelsTest(unittest.TestCase):
def setUp(self):
......@@ -96,31 +97,31 @@ class MultiChoiceTest(unittest.TestCase):
multichoice_file = os.path.dirname(__file__)+"/test_files/multichoice.xml"
test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs)
correct_answers = {'1_2_1':'choice_foil3'}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
false_answers = {'1_2_1':'choice_foil2'}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
def test_MC_bare_grades(self):
multichoice_file = os.path.dirname(__file__)+"/test_files/multi_bare.xml"
test_lcp = lcp.LoncapaProblem(open(multichoice_file), '1', system=i4xs)
correct_answers = {'1_2_1':'choice_2'}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
false_answers = {'1_2_1':'choice_1'}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
def test_TF_grade(self):
truefalse_file = os.path.dirname(__file__)+"/test_files/truefalse.xml"
test_lcp = lcp.LoncapaProblem(open(truefalse_file), '1', system=i4xs)
correct_answers = {'1_2_1':['choice_foil2', 'choice_foil1']}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
false_answers = {'1_2_1':['choice_foil1']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
false_answers = {'1_2_1':['choice_foil1', 'choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
false_answers = {'1_2_1':['choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
false_answers = {'1_2_1':['choice_foil1', 'choice_foil2', 'choice_foil3']}
self.assertEquals(test_lcp.grade_answers(false_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect')
class ImageResponseTest(unittest.TestCase):
def test_ir_grade(self):
......@@ -131,8 +132,8 @@ class ImageResponseTest(unittest.TestCase):
test_answers = {'1_2_1':'[500,20]',
'1_2_2':'[250,300]',
}
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
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')
class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self):
......@@ -220,8 +221,8 @@ class SymbolicResponseTest(unittest.TestCase):
</mstyle>
</math>''',
}
self.assertEquals(test_lcp.grade_answers(correct_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(wrong_answers)['1_2_1'], 'incorrect')
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect')
class OptionResponseTest(unittest.TestCase):
'''
......@@ -237,8 +238,37 @@ class OptionResponseTest(unittest.TestCase):
test_answers = {'1_2_1':'True',
'1_2_2':'True',
}
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_1'], 'correct')
self.assertEquals(test_lcp.grade_answers(test_answers)['1_2_2'], 'incorrect')
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')
class FormulaResponseWithHintTest(unittest.TestCase):
'''
Test Formula response problem with a hint
This problem also uses calc.
'''
def test_or_grade(self):
problem_file = os.path.dirname(__file__)+"/test_files/formularesponse_with_hint.xml"
test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs)
correct_answers = {'1_2_1':'2.5*x-5.0'}
test_answers = {'1_2_1':'0.4*x-5.0'}
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
cmap = test_lcp.grade_answers(test_answers)
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
self.assertTrue('You have inverted' in cmap.get_hint('1_2_1'))
class StringResponseWithHintTest(unittest.TestCase):
'''
Test String response problem with a hint
'''
def test_or_grade(self):
problem_file = os.path.dirname(__file__)+"/test_files/stringresponse_with_hint.xml"
test_lcp = lcp.LoncapaProblem(open(problem_file), '1', system=i4xs)
correct_answers = {'1_2_1':'Michigan'}
test_answers = {'1_2_1':'Minnesota'}
self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct')
cmap = test_lcp.grade_answers(test_answers)
self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect')
self.assertTrue('St. Paul' in cmap.get_hint('1_2_1'))
#-----------------------------------------------------------------------------
# Grading tests
......
#!/bin/bash
#!/usr/bin/env bash
set -e
# posix compliant sanity check
if [ -z $BASH ] || [ $BASH = "/bin/sh" ]; then
echo "Please use the bash interpreter to run this script"
exit 1
fi
trap "ouch" ERR
ouch() {
......@@ -14,6 +21,7 @@ ouch() {
script again with the -v flag.
EOL
printf '\E[0m'
}
error() {
......@@ -28,6 +36,7 @@ usage() {
Usage: $PROG [-c] [-v] [-h]
-c compile scipy and numpy
-s --system-site-packages for virtualenv
-v set -x + spew
-h this
......@@ -48,21 +57,45 @@ EO
clone_repos() {
cd "$BASE"
output "Cloning mitx"
if [[ -d "$BASE/mitx" ]]; then
mv "$BASE/mitx" "${BASE}/mitx.bak.$$"
if [[ -d "$BASE/mitx/.git" ]]; then
output "Pulling mitx"
cd "$BASE/mitx"
git pull >>$LOG
else
output "Cloning mitx"
if [[ -d "$BASE/mitx" ]]; then
mv "$BASE/mitx" "${BASE}/mitx.bak.$$"
fi
git clone git@github.com:MITx/mitx.git >>$LOG
fi
git clone git@github.com:MITx/mitx.git >>$LOG
output "Cloning askbot-devel"
if [[ -d "$BASE/askbot-devel" ]]; then
mv "$BASE/askbot-devel" "${BASE}/askbot-devel.bak.$$"
cd "$BASE"
if [[ -d "$BASE/askbot-devel/.git" ]]; then
output "Pulling askbot-devel"
cd "$BASE/askbot-devel"
git pull >>$LOG
else
output "Cloning askbot-devel"
if [[ -d "$BASE/askbot-devel" ]]; then
mv "$BASE/askbot-devel" "${BASE}/askbot-devel.bak.$$"
fi
git clone git@github.com:MITx/askbot-devel >>$LOG
fi
git clone git@github.com:MITx/askbot-devel >>$LOG
output "Cloning data"
if [[ -d "$BASE/data" ]]; then
mv "$BASE/data" "${BASE}/data.bak.$$"
cd "$BASE"
if [[ -d "$BASE/data/.hg" ]]; then
output "Pulling data"
cd "$BASE/data"
hg pull >>$LOG
hg update >>$LOG
else
output "Cloning data"
if [[ -d "$BASE/data" ]]; then
mv "$BASE/data" "${BASE}/data.bak.$$"
fi
hg clone ssh://hg-content@gp.mitx.mit.edu/data >>$LOG
fi
hg clone ssh://hg-content@gp.mitx.mit.edu/data >>$LOG
}
PROG=${0##*/}
......@@ -81,7 +114,7 @@ if [[ $EUID -eq 0 ]]; then
usage
exit 1
fi
ARGS=$(getopt "cvh" "$*")
ARGS=$(getopt "cvhs" "$*")
if [[ $? != 0 ]]; then
usage
exit 1
......@@ -93,6 +126,10 @@ while true; do
compile=true
shift
;;
-s)
systempkgs=true
shift
;;
-v)
set -x
verbose=true
......@@ -123,7 +160,7 @@ cat<<EO
To compile scipy and numpy from source use the -c option
STDOUT is redirected to /var/tmp/install.log, run
Most of STDOUT is redirected to /var/tmp/install.log, run
$ tail -f /var/tmp/install.log
to monitor progress
......@@ -211,8 +248,13 @@ esac
output "Installing rvm and ruby"
curl -sL get.rvm.io | bash -s stable
source $RUBY_DIR/scripts/rvm
rvm install $RUBY_VER
virtualenv "$PYTHON_DIR"
# skip the intro
LESS="-E" rvm install $RUBY_VER
if [[ -n $systempkgs ]]; then
virtualenv --system-site-packages "$PYTHON_DIR"
else
virtualenv "$PYTHON_DIR"
fi
source $PYTHON_DIR/bin/activate
output "Installing gem bundler"
gem install bundler
......@@ -251,23 +293,38 @@ mkdir "$BASE/log" || true
mkdir "$BASE/db" || true
cat<<END
Success!!
To start using Django you will need
to activate the local Python and Ruby
environment:
To start using Django you will need to activate the local Python
and Ruby environment (at this time rvm only supports bash) :
$ source $RUBY_DIR/scripts/rvm
$ source $PYTHON_DIR/bin/activate
To initialize and start a local instance of Django:
To initialize Django
$ cd $BASE/mitx
$ django-admin.py syncdb --settings=envs.dev --pythonpath=.
$ django-admin.py migrate --settings=envs.dev --pythonpath=.
$ django-admin.py runserver --settings=envs.dev --pythonpath=.
$ rake django-admin[syncdb]
$ rake django-admin[migrate]
To start the Django on port 8000
$ rake lms
Or to start Django on a different <port#>
$ rake django-admin[runserver,lms,dev,<port#>]
If the Django development server starts properly you
should see:
Development server is running at http://127.0.0.1:<port#>/
Quit the server with CONTROL-C.
Connect your browser to http://127.0.0.1:<port#> to
view the Django site.
END
exit 0
This directory contains some high level documentation for the code. We should strive to keep it up-to-date, but don't take it as the absolute truth.
A good place to start is 'overview.md'
Scope
This document describes code quality standards for the i4x
system.
1. Coding Standards
Code falls into four categories:
* Deployed. Running on a live server.
* Production. Intended for deployment.
* Scaffolding. Intended to define interfaces for future work, and
minimal implementations to support further development.
* Prototype. Experimental new features.
1.1 Deployed
The standards for deployed code are identical to production. In
general, we tend to do either:
1) Perform a final verification QA cycle on changed parts of code
before deploying.
2) Use code on a staging or internal server for a week before
deploying.
1.2 Production
All production code must be peer-reviewed. The code must meet the
following standards:
1) Test Suite. Code must have reasonable, although not complete, test
coverage.
2) Consistent. Code must follow PEP8
3) Clean Abstractions.
4) Future-Compatible. Code must not be incompatible with the
long-term vision of either the codebase or of edX.
5) Properly Documented
6) Maintainable and deployable
7) Robust.
All code paths must be manually or automatically verified.
1.3 Scaffolding
All scaffolding code should be peer-reviewed. The code must meet the
following standards:
1) Testable. We do not require test coverage, but we do require the
code to be structured such that it is possible to build tests.
2) Consistent. Code must follow PEP8
3) Clean abstractions or obvious throw-away code. One of the goals
of scaffolding is to define proper abstractions.
4) Future-Compatible. Code must not be incompatible with the
long-term vision of either the codebase or of edX.
5) Somewhat documented
6) Unpluggable. There should be a setting to disable scaffolding code.
By default, and by policy, it should never be enabled on production
servers.
7) Purpose. The scaffolding must provide a clean reason for existence
(e.g. define a specific interface, etc.)
1.4 Prototype
Prototype code should live in a separate branch. It should strive
to follow PEP8, be readable, testable, and future-proof, but we have
no hard standards.
2. Process Standards
* Code should be integrated in small pull requests. Large commits
should be broken down into small commits for integration.
* Every piece of production and deployed code must be reviewed prior
to integration.
* Anyone on the edX team competent to review a piece of code may
review it (this may change as the team grows).
* Each contributor is responsible for finding a person to review their
code. If it is not clear to the contributor who is appropriate, each
project has an owner
3. Documentation Standards
* Whenever possible, documentation should live in code.
* When impossible, it should live in the github repo.
* Discussion should live on github, Basecamp or Pivotal, depending on
context.
# Documentation for edX code (mitx repo)
This document explains the general structure of the edX platform, and defines some of the acronyms and terms you'll see flying around in the code.
## Assumptions:
You should be familiar with the following. If you're not, go read some docs...
- python
- django
- javascript
- html, xml -- xpath, xslt
- css
- git
- mako templates -- we use these instead of django templates, because they support embedding real python.
## Other relevant terms
- CAPA -- lon-capa.org -- content management system that has defined a standard for online learning and assessment materials. Many of our materials follow this standard.
- TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive.
- lcp = loncapa problem
## Parts of the system
- LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc.
- CMS -- Course Management System. The instructor-facing parts of the system. Allows instructors to see and modify their course, add lectures, problems, reorder things, etc.
- Askbot -- the discussion forums. We have a custom fork of this project. We're also hoping to replace it with something better later. (e.g. need support for multiple classes, etc)
- Data. In the data/ dir. There is currently a single `course.xml` file that describes an entire course. Speaking of which...
- Courses. A course is broken up into Chapters ("week 1", "week 2", etc). A chapter is broken up into Sections ("Lecture 1", "Simple Circuits Exercises", "HW1", etc). A section can contain modules: Problems, Html, Videos, Verticals, or Sequences.
- Problems: specified in problem files. May have python scripts embedded to both generate random parameters and check answers. Also allows specifying things like tolerance or precision in answers
- Html: any html - often description, or links to outside resources
- Videos: links to youtube or elsewhere
- Verticals: a nesting tag: collect several videos, problems, html modules and display them vertically.
- Sequences: a sequence of modules, displayed with a horizontal navigation bar, displaying one component at a time.
- see `data/course.xml` for more examples
## High Level Entities in the code
### Common libraries
- x_modules -- generic learning modules. *x* can be sequence, video, template, html, vertical, capa, etc. These are the things that one puts inside sections in the course structure. Modules know how to render themselves to html, how to score themselves, and handle ajax calls from the front end.
- x_modules take a 'system context' parameter, which helps isolate xmodules from any particular application, so they can be used in many places. The modules should make no references to Django (though there are still a few left). The system context knows how to render things, track events, complain about 404s, etc.
- TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here)
- in `common/lib/xmodule`
- capa modules -- defines `LoncapaProblem` and many related things.
- in `common/lib/capa`
### LMS
The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`.
- We use the Django Auth system, including the is_staff and is_superuser flags. User profiles and related code lives in `lms/djangoapps/student/`. There is support for groups of students (e.g. 'want emails about future courses', 'have unenrolled', etc) in `lms/djangoapps/student/models.py`.
- `StudentModule` -- keeps track of where a particular student is in a module (problem, video, html)--what's their grade, have they started, are they done, etc. [This is only partly implemented so far.]
- `lms/djangoapps/courseware/models.py`
- Core rendering path:
- `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits).
- Calls `render_accordion` to render the "accordion"--the display of the course structure.
- To render the current module, calls `module_render.py:render_x_module()`, which gets the `StudentModule` instance, and passes the `StudentModule` state and other system context to the module constructor the get an instance of the appropriate module class for this user.
- calls the module's `.get_html()` method. If the module has nested submodules, render_x_module() will be called again for each.
- ajax calls go to `module_render.py:modx_dispatch()`, which passes it to the module's `handle_ajax()` function, and then updates the grade and state if they changed.
- [This diagram](https://github.com/MITx/mitx/wiki/MITx-Architecture) visually shows how the clients communicate with problems + modules.
- See `lms/urls.py` for the wirings of urls to views.
- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`.
### Other modules
- Wiki -- in `lms/djangoapps/simplewiki`. Has some markdown extentions for embedding circuits, videos, etc.
## Testing
See `testing.md`.
## TODO:
- update lms/envs/README.txt
- describe our production environment
- describe the front-end architecture, tools, etc. Starting point: `lms/static`
---
Note: this file uses markdown. To convert to html, run:
markdown2 overview.md > overview.html
# Testing
Testing is good. Here is some useful info about how we set up tests--
### Backend code:
- TODO
### Frontend code:
- TODO
......@@ -89,7 +89,7 @@ def login_user(request, error=""):
@ensure_csrf_cookie
def logout_user(request):
''' HTTP request to log in the user. Redirects to marketing page'''
''' HTTP request to log out the user. Redirects to marketing page'''
logout(request)
return redirect('/')
......
......@@ -37,6 +37,7 @@ PERFSTATS = False
MITX_FEATURES = {
'SAMPLE' : False,
'USE_DJANGO_PIPELINE' : True,
'DISPLAY_HISTOGRAMS_TO_STAFF' : True,
}
# Used for A/B testing
......@@ -287,13 +288,15 @@ PIPELINE_CSS = {
}
}
PIPELINE_ALWAYS_RECOMPILE = ['sass/application.scss', 'sass/marketing.scss', 'sass/marketing-ie.scss', 'sass/print.scss']
PIPELINE_JS = {
'application': {
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')],
'output_filename': 'js/application.js'
},
'spec': {
'source_filenames': [pth.replace('static/', '') for pth in glob2.glob('static/coffee/spec/**/*.coffee')],
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')],
'output_filename': 'js/spec.js'
}
}
......
......@@ -27,7 +27,8 @@ DEBUG = True
ENABLE_MULTICOURSE = True # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = True
MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
# MITX_FEATURES['USE_DJANGO_PIPELINE'] = False
MITX_FEATURES['DISPLAY_HISTOGRAMS_TO_STAFF'] = False
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics',
......
......@@ -20,7 +20,7 @@ INSTALLED_APPS = [
# Nose Test Runner
INSTALLED_APPS += ['django_nose']
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive']
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', '--cover-inclusive', '--cover-html-dir', os.environ['NOSE_COVER_HTML_DIR']]
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
......
This is a library for edx4edx, allowing users to practice writing problems.
......@@ -5,7 +5,7 @@ import string
import traceback
from django.conf import settings
import courseware.capa.capa_problem as lcp
import capa.capa_problem as lcp
from dogfood.views import update_problem
def GenID(length=8, chars=string.letters + string.digits):
......@@ -44,9 +44,9 @@ def check_problem_code(ans,the_lcp,correct_answers,false_answers):
fp = the_lcp.system.filestore.open('problems/%s.xml' % pfn)
test_lcp = lcp.LoncapaProblem(fp, '1', system=the_lcp.system)
if not (test_lcp.grade_answers(correct_answers)['1_2_1']=='correct'):
if not (test_lcp.grade_answers(correct_answers).get_correctness('1_2_1')=='correct'):
is_ok = False
if (test_lcp.grade_answers(false_answers)['1_2_1']=='correct'):
if (test_lcp.grade_answers(false_answers).get_correctness('1_2_1')=='correct'):
is_ok = False
except Exception,err:
is_ok = False
......
......@@ -21,7 +21,6 @@ from django.http import HttpResponse
from django.shortcuts import redirect
from mitxmako.shortcuts import render_to_response, render_to_string
import courseware.capa.calc
import track.views
from lxml import etree
......@@ -34,7 +33,8 @@ from util.cache import cache
from util.views import accepts
import courseware.content_parser as content_parser
import courseware.modules
#import courseware.modules
import xmodule
log = logging.getLogger("mitx.courseware")
......@@ -184,7 +184,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
filestore = OSFS(settings.DATA_DIR + xp),
#role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
)
instance=courseware.modules.get_module_class(module)(system,
instance=xmodule.get_module_class(module)(system,
xml,
id,
state=None)
......
......@@ -6,6 +6,10 @@
<section class="action">
<input type="hidden" name="problem_id" value="1">
<input type="text" name="input_example_1" id="input_example_1" value="" class="math" />
<span id="display_example_1"></span>
<span id="input_example_1_dynamath"></span>
<input class="check" type="button" value="Check">
<input class="reset" type="button" value="Reset">
<input class="save" type="button" value="Save">
......
......@@ -33,16 +33,16 @@ describe 'Calculator', ->
describe 'toggle', ->
it 'toggle the calculator and focus the input', ->
spyOn $.fn, 'focus'
@calculator.toggle()
@calculator.toggle(jQuery.Event("click"))
expect($('li.calc-main')).toHaveClass('open')
expect($('#calculator_wrapper #calculator_input').focus).toHaveBeenCalled()
it 'toggle the close button on the calculator button', ->
@calculator.toggle()
@calculator.toggle(jQuery.Event("click"))
expect($('.calc')).toHaveClass('closed')
@calculator.toggle()
@calculator.toggle(jQuery.Event("click"))
expect($('.calc')).not.toHaveClass('closed')
describe 'helpToggle', ->
......
......@@ -5,16 +5,6 @@ describe 'Courseware', ->
Courseware.start()
expect(window.Navigation).toHaveBeenCalled()
it 'create the calculator', ->
spyOn(window, 'Calculator')
Courseware.start()
expect(window.Calculator).toHaveBeenCalled()
it 'creates the FeedbackForm', ->
spyOn(window, 'FeedbackForm')
Courseware.start()
expect(window.FeedbackForm).toHaveBeenCalled()
it 'binds the Logger', ->
spyOn(Logger, 'bind')
Courseware.start()
......
......@@ -30,16 +30,17 @@ jasmine.stubRequests = ->
jasmine.stubYoutubePlayer = ->
YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode',
'getCurrentTime', 'getPlayerState', 'loadVideoById', 'playVideo', 'pauseVideo', 'seekTo']
'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById',
'playVideo', 'pauseVideo', 'seekTo']
jasmine.stubVideoPlayer = (context, enableParts) ->
jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) ->
enableParts = [enableParts] unless $.isArray(enableParts)
suite = context.suite
currentPartName = suite.description while suite = suite.parentSuite
enableParts.push currentPartName
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoProgressSlider']
for part in ['VideoCaption', 'VideoSpeedControl', 'VideoVolumeControl', 'VideoProgressSlider']
unless $.inArray(part, enableParts) >= 0
spyOn window, part
......@@ -48,7 +49,8 @@ jasmine.stubVideoPlayer = (context, enableParts) ->
YT.Player = undefined
context.video = new Video 'example', '.75:abc123,1.0:def456'
jasmine.stubYoutubePlayer()
return new VideoPlayer context.video
if createPlayer
return new VideoPlayer context.video
spyOn(window, 'onunload')
......
......@@ -11,7 +11,7 @@ describe 'Histogram', ->
describe 'calculate', ->
beforeEach ->
@histogram = new Histogram(1, [[1, 1], [2, 2], [3, 3]])
@histogram = new Histogram(1, [[null, 1], [1, 1], [2, 2], [3, 3]])
it 'store the correct value for data', ->
expect(@histogram.data).toEqual [[1, Math.log(2)], [2, Math.log(3)], [3, Math.log(4)]]
......
describe 'Problem', ->
beforeEach ->
# Stub MathJax
window.MathJax = { Hub: { Queue: -> } }
window.MathJax =
Hub: jasmine.createSpyObj('MathJax.Hub', ['getAllJax', 'Queue'])
Callback: jasmine.createSpyObj('MathJax.Callback', ['After'])
@stubbedJax = root: jasmine.createSpyObj('jax.root', ['toMathML'])
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
window.update_schematics = ->
loadFixtures 'problem.html'
......@@ -25,8 +29,8 @@ describe 'Problem', ->
describe 'bind', ->
beforeEach ->
spyOn MathJax.Hub, 'Queue'
spyOn window, 'update_schematics'
MathJax.Hub.getAllJax.andReturn [@stubbedJax]
@problem = new Problem 1, '/problem/url/'
it 'set mathjax typeset', ->
......@@ -50,6 +54,12 @@ describe 'Problem', ->
it 'bind the save button', ->
expect($('section.action input.save')).toHandleWith 'click', @problem.save
it 'bind the math input', ->
expect($('input.math')).toHandleWith 'keyup', @problem.refreshMath
it 'display the math input', ->
expect(@stubbedJax.root.toMathML).toHaveBeenCalled()
describe 'render', ->
beforeEach ->
@problem = new Problem 1, '/problem/url/'
......@@ -223,6 +233,30 @@ describe 'Problem', ->
@problem.save()
expect(window.alert).toHaveBeenCalledWith 'Saved'
describe 'refreshMath', ->
beforeEach ->
@problem = new Problem 1, '/problem/url/'
@stubbedJax.root.toMathML.andReturn '<MathML>'
$('#input_example_1').val 'E=mc^2'
describe 'when there is no exception', ->
beforeEach ->
@problem.refreshMath target: $('#input_example_1').get(0)
it 'should convert and display the MathML object', ->
expect(MathJax.Hub.Queue).toHaveBeenCalledWith ['Text', @stubbedJax, 'E=mc^2']
it 'should display debug output in hidden div', ->
expect($('#input_example_1_dynamath')).toHaveValue '<MathML>'
describe 'when there is an exception', ->
beforeEach ->
@stubbedJax.root.toMathML.andThrow {restart: true}
@problem.refreshMath target: $('#input_example_1').get(0)
it 'should queue up the exception', ->
expect(MathJax.Callback.After).toHaveBeenCalledWith [@problem.refreshMath, @stubbedJax], true
describe 'refreshAnswers', ->
beforeEach ->
@problem = new Problem 1, '/problem/url/'
......
......@@ -24,7 +24,6 @@ describe 'Sequence', ->
expect(titles).toEqual ['Video 1', 'Video 2', 'Sample Problem']
it 'bind the page events', ->
expect(@sequence.element).toHandleWith 'contentChanged', @sequence.toggleArrows
expect($('#sequence-list a')).toHandleWith 'click', @sequence.goto
it 'render the active sequence content', ->
......@@ -76,6 +75,7 @@ describe 'Sequence', ->
spyOn $, 'postWithPrefix'
@sequence = new Sequence '1', @items, 'sequence'
spyOnEvent @sequence.element, 'contentChanged'
spyOn(@sequence, 'toggleArrows').andCallThrough()
describe 'with a different position than the current one', ->
beforeEach ->
......@@ -105,6 +105,9 @@ describe 'Sequence', ->
it 'update the position', ->
expect(@sequence.position).toEqual 1
it 're-update the arrows', ->
expect(@sequence.toggleArrows).toHaveBeenCalled()
it 'trigger contentChanged event', ->
expect('contentChanged').toHaveBeenTriggeredOn @sequence.element
......
describe 'VideoPlayer', ->
beforeEach ->
jasmine.stubVideoPlayer @
jasmine.stubVideoPlayer @, [], false
afterEach ->
YT.Player = undefined
......@@ -11,69 +11,94 @@ describe 'VideoPlayer', ->
spyOn YT, 'Player'
$.fn.qtip.andCallFake ->
$(this).data('qtip', true)
$('.video').append $('<div class="hide-subtitles" />')
@player = new VideoPlayer @video
$('.video').append $('<div class="add-fullscreen" /><div class="hide-subtitles" />')
it 'instanticate current time to zero', ->
expect(@player.currentTime).toEqual 0
describe 'always', ->
beforeEach ->
@player = new VideoPlayer @video
it 'set the element', ->
expect(@player.element).toBe '#video_example'
it 'instanticate current time to zero', ->
expect(@player.currentTime).toEqual 0
it 'create video control', ->
expect(window.VideoControl).toHaveBeenCalledWith @player
it 'set the element', ->
expect(@player.element).toBe '#video_example'
it 'create video caption', ->
expect(window.VideoCaption).toHaveBeenCalledWith @player, 'def456'
it 'create video control', ->
expect(window.VideoControl).toHaveBeenCalledWith @player
it 'create video speed control', ->
expect(window.VideoSpeedControl).toHaveBeenCalledWith @player, ['0.75', '1.0']
it 'create video caption', ->
expect(window.VideoCaption).toHaveBeenCalledWith @player, 'def456'
it 'create video progress slider', ->
expect(window.VideoProgressSlider).toHaveBeenCalledWith @player
it 'create video speed control', ->
expect(window.VideoSpeedControl).toHaveBeenCalledWith @player, ['0.75', '1.0']
it 'create Youtube player', ->
expect(YT.Player).toHaveBeenCalledWith 'example'
playerVars:
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
videoId: 'def456'
events:
onReady: @player.onReady
onStateChange: @player.onStateChange
it 'create video progress slider', ->
expect(window.VideoProgressSlider).toHaveBeenCalledWith @player
it 'bind to seek event', ->
expect($(@player)).toHandleWith 'seek', @player.onSeek
it 'create Youtube player', ->
expect(YT.Player).toHaveBeenCalledWith 'example'
playerVars:
controls: 0
wmode: 'transparent'
rel: 0
showinfo: 0
enablejsapi: 1
videoId: 'def456'
events:
onReady: @player.onReady
onStateChange: @player.onStateChange
it 'bind to updatePlayTime event', ->
expect($(@player)).toHandleWith 'updatePlayTime', @player.onUpdatePlayTime
it 'bind to seek event', ->
expect($(@player)).toHandleWith 'seek', @player.onSeek
it 'bidn to speedChange event', ->
expect($(@player)).toHandleWith 'speedChange', @player.onSpeedChange
it 'bind to updatePlayTime event', ->
expect($(@player)).toHandleWith 'updatePlayTime', @player.onUpdatePlayTime
it 'bind to play event', ->
expect($(@player)).toHandleWith 'play', @player.onPlay
it 'bidn to speedChange event', ->
expect($(@player)).toHandleWith 'speedChange', @player.onSpeedChange
it 'bind to paused event', ->
expect($(@player)).toHandleWith 'pause', @player.onPause
it 'bind to play event', ->
expect($(@player)).toHandleWith 'play', @player.onPlay
it 'bind to ended event', ->
expect($(@player)).toHandleWith 'ended', @player.onPause
it 'bind to paused event', ->
expect($(@player)).toHandleWith 'pause', @player.onPause
it 'bind to key press', ->
expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen
it 'bind to ended event', ->
expect($(@player)).toHandleWith 'ended', @player.onPause
it 'bind to fullscreen switching button', ->
expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
it 'bind to key press', ->
expect($(document)).toHandleWith 'keyup', @player.bindExitFullScreen
it 'bind to fullscreen switching button', ->
console.debug $('.add-fullscreen')
expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen
describe 'when not on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn false
$('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer @video
it 'add the tooltip to fullscreen and subtitle button', ->
expect($('.add-fullscreen')).toHaveData 'qtip'
expect($('.hide-subtitles')).toHaveData 'qtip'
it 'create video volume control', ->
expect(window.VideoVolumeControl).toHaveBeenCalledWith @player
describe 'when on a touch based device', ->
beforeEach ->
spyOn(window, 'onTouchBasedDevice').andReturn true
$('.add-fullscreen, .hide-subtitles').removeData 'qtip'
@player = new VideoPlayer @video
it 'does not add the tooltip to fullscreen and subtitle button', ->
expect($('.add-fullscreen')).not.toHaveData 'qtip'
expect($('.hide-subtitles')).not.toHaveData 'qtip'
it 'does not create video volume control', ->
expect(window.VideoVolumeControl).not.toHaveBeenCalled()
describe 'onReady', ->
beforeEach ->
@video.embed()
......@@ -387,3 +412,17 @@ describe 'VideoPlayer', ->
it 'delegate to the video', ->
expect(@player.currentSpeed()).toEqual '3.0'
describe 'volume', ->
beforeEach ->
@player = new VideoPlayer @video
@player.player.getVolume.andReturn 42
describe 'without value', ->
it 'return current volume', ->
expect(@player.volume()).toEqual 42
describe 'with value', ->
it 'set player volume', ->
@player.volume(60)
expect(@player.player.setVolume).toHaveBeenCalledWith(60)
......@@ -18,7 +18,7 @@ describe 'VideoProgressSlider', ->
stop: @slider.onStop
it 'build the seek handle', ->
expect(@slider.handle).toBe '.ui-slider-handle'
expect(@slider.handle).toBe '.slider .ui-slider-handle'
expect($.fn.qtip).toHaveBeenCalledWith
content: "0:00"
position:
......
......@@ -3,8 +3,6 @@ describe 'VideoSpeedControl', ->
@player = jasmine.stubVideoPlayer @
$('.speeds').remove()
afterEach ->
describe 'constructor', ->
describe 'always', ->
beforeEach ->
......
describe 'VideoVolumeControl', ->
beforeEach ->
@player = jasmine.stubVideoPlayer @
$('.volume').remove()
describe 'constructor', ->
beforeEach ->
spyOn($.fn, 'slider')
@volumeControl = new VideoVolumeControl @player
it 'initialize previousVolume to 100', ->
expect(@volumeControl.previousVolume).toEqual 100
it 'render the volume control', ->
expect($('.secondary-controls').html()).toContain """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""
it 'create the slider', ->
expect($.fn.slider).toHaveBeenCalledWith
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @volumeControl.onChange
slide: @volumeControl.onChange
it 'bind the volume control', ->
expect($(@player)).toHandleWith 'ready', @volumeControl.onReady
expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute
expect($('.volume')).not.toHaveClass 'open'
$('.volume').mouseenter()
expect($('.volume')).toHaveClass 'open'
$('.volume').mouseleave()
expect($('.volume')).not.toHaveClass 'open'
describe 'onReady', ->
beforeEach ->
@volumeControl = new VideoVolumeControl @player
spyOn $.fn, 'slider'
spyOn(@player, 'volume').andReturn 60
@volumeControl.onReady()
it 'set the max value of the slider', ->
expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 60
describe 'onChange', ->
beforeEach ->
spyOn @player, 'volume'
@volumeControl = new VideoVolumeControl @player
describe 'when the new volume is more than 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 60
it 'set the player volume', ->
expect(@player.volume).toHaveBeenCalledWith 60
it 'remote muted class', ->
expect($('.volume')).not.toHaveClass 'muted'
describe 'when the new volume is 0', ->
beforeEach ->
@volumeControl.onChange undefined, value: 0
it 'set the player volume', ->
expect(@player.volume).toHaveBeenCalledWith 0
it 'add muted class', ->
expect($('.volume')).toHaveClass 'muted'
describe 'toggleMute', ->
beforeEach ->
spyOn @player, 'volume'
@volumeControl = new VideoVolumeControl @player
describe 'when the current volume is more than 0', ->
beforeEach ->
@player.volume.andReturn 60
@volumeControl.toggleMute()
it 'save the previous volume', ->
expect(@volumeControl.previousVolume).toEqual 60
it 'set the player volume', ->
expect(@player.volume).toHaveBeenCalledWith 0
describe 'when the current volume is 0', ->
beforeEach ->
@player.volume.andReturn 0
@volumeControl.previousVolume = 60
@volumeControl.toggleMute()
it 'set the player volume to previous volume', ->
expect(@player.volume).toHaveBeenCalledWith 60
......@@ -6,7 +6,8 @@ class @Calculator
$('div.help-wrapper a').hover(@helpToggle).click (e) ->
e.preventDefault()
toggle: ->
toggle: (event) ->
event.preventDefault()
$('li.calc-main').toggleClass 'open'
$('#calculator_wrapper #calculator_input').focus()
if $('.calc.closed').length
......
......@@ -4,8 +4,6 @@ class @Courseware
constructor: ->
Courseware.prefix = $("meta[name='path_prefix']").attr('content')
new Navigation
new Calculator
new FeedbackForm
Logger.bind()
@bind()
@render()
......
......@@ -8,6 +8,7 @@ class @Histogram
calculate: ->
for [score, count] in @rawData
continue if score == null
log_count = Math.log(count + 1)
@data.push [score, log_count]
@xTicks.push [score, score.toString()]
......
......@@ -17,6 +17,8 @@ $ ->
$("a[rel*=leanModal]").leanModal()
$('#csrfmiddlewaretoken').attr 'value', $.cookie('csrftoken')
new Calculator
new FeedbackForm
if $('body').hasClass('courseware')
Courseware.start()
......
......@@ -15,6 +15,7 @@ class @Problem
@$('section.action input.reset').click @reset
@$('section.action input.show').click @show
@$('section.action input.save').click @save
@$('input.math').keyup(@refreshMath).each(@refreshMath)
render: (content) ->
if content
......@@ -44,14 +45,14 @@ class @Problem
$.each response, (key, value) =>
if $.isArray(value)
for choice in value
@$("label[for='input_#{key}_#{choice}']").attr
correct_answer: 'true'
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
else
@$("#answer_#{key}").text(value)
@$("#answer_#{key}, #solution_#{key}").html(value)
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@$('.show').val 'Hide Answer'
@element.addClass 'showed'
else
@$('[id^=answer_]').text ''
@$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null
@element.removeClass 'showed'
@$('.show').val 'Show Answer'
......@@ -62,6 +63,20 @@ class @Problem
if response.success
alert 'Saved'
refreshMath: (event, element) =>
element = event.target unless element
target = "display_#{element.id.replace(/^input_/, '')}"
if jax = MathJax.Hub.getAllJax(target)[0]
MathJax.Hub.Queue ['Text', jax, $(element).val()]
try
output = jax.root.toMathML ''
$("##{element.id}_dynamath").val(output)
catch exception
throw exception unless exception.restart
MathJax.Callback.After [@refreshMath, jax], exception.restart
refreshAnswers: =>
@$('input.schematic').each (index, element) ->
element.schematic.update_value()
......
......@@ -9,7 +9,6 @@ class @Sequence
$(selector, @element)
bind: ->
@element.bind 'contentChanged', @toggleArrows
@$('#sequence-list a').click @goto
buildNavigation: ->
......@@ -43,6 +42,7 @@ class @Sequence
MathJax.Hub.Queue(["Typeset", MathJax.Hub])
@position = new_position
@toggleArrows()
@element.trigger 'contentChanged'
goto: (event) =>
......
......@@ -30,6 +30,7 @@ class @VideoPlayer
render: ->
new VideoControl @
new VideoCaption @, @video.youtubeId('1.0')
new VideoVolumeControl @ unless onTouchBasedDevice()
new VideoSpeedControl @, @video.speeds
new VideoProgressSlider @
@player = new YT.Player @video.id,
......@@ -132,3 +133,9 @@ class @VideoPlayer
currentSpeed: ->
@video.speed
volume: (value) ->
if value?
@player.setVolume value
else
@player.getVolume()
......@@ -17,7 +17,7 @@ class @VideoProgressSlider
@buildHandle()
buildHandle: ->
@handle = @$('.ui-slider-handle')
@handle = @$('.slider .ui-slider-handle')
@handle.qtip
content: "#{Time.format(@slider.slider('value'))}"
position:
......
class @VideoVolumeControl
constructor: (@player) ->
@previousVolume = 100
@render()
@bind()
$: (selector) ->
@player.$(selector)
bind: ->
$(@player).bind('ready', @onReady)
@$('.volume').mouseenter ->
$(this).addClass('open')
@$('.volume').mouseleave ->
$(this).removeClass('open')
@$('.volume>a').click(@toggleMute)
render: ->
@$('.secondary-controls').prepend """
<div class="volume">
<a href="#"></a>
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
</div>
"""
@slider = @$('.volume-slider').slider
orientation: "vertical"
range: "min"
min: 0
max: 100
value: 100
change: @onChange
slide: @onChange
onReady: =>
@slider.slider 'option', 'max', @player.volume()
onChange: (event, ui) =>
@player.volume ui.value
@$('.secondary-controls .volume').toggleClass 'muted', ui.value == 0
toggleMute: =>
if @player.volume() > 0
@previousVolume = @player.volume()
@slider.slider 'option', 'value', 0
else
@slider.slider 'option', 'value', @previousVolume
......@@ -286,6 +286,87 @@ section.course-content {
}
}
div.volume {
float: left;
position: relative;
&.open {
.volume-slider-container {
display: block;
opacity: 1;
}
}
&.muted {
&>a {
background: url('../images/mute.png') 10px center no-repeat;
}
}
> a {
background: url('../images/volume.png') 10px center no-repeat;
border-right: 1px solid #000;
@include box-shadow(1px 0 0 #555, inset 1px 0 0 #555);
@include clearfix();
color: #fff;
cursor: pointer;
display: block;
height: 46px;
margin-right: 0;
padding-left: 15px;
position: relative;
@include transition();
-webkit-font-smoothing: antialiased;
width: 30px;
&:hover, &:active, &:focus {
background-color: #444;
}
}
.volume-slider-container {
@include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444);
@include transition();
background-color: #444;
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0;
position: absolute;
width: 45px;
height: 125px;
margin-left: -1px;
z-index: 10;
.volume-slider {
height: 100px;
border: 0;
width: 5px;
margin: 14px auto;
background: #666;
border: 1px solid #000;
@include box-shadow(0 1px 0 #333);
a.ui-slider-handle {
background: $mit-red url(../images/slider-handle.png) center center no-repeat;
@include background-size(50%);
border: 1px solid darken($mit-red, 20%);
@include border-radius(15px);
@include box-shadow(inset 0 1px 0 lighten($mit-red, 10%));
cursor: pointer;
height: 15px;
left: -6px;
@include transition(height 2.0s ease-in-out, width 2.0s ease-in-out);
width: 15px;
}
.ui-slider-range {
background: #ddd;
}
}
}
}
a.add-fullscreen {
background: url(../images/fullscreen.png) center no-repeat;
border-right: 1px solid #000;
......
......@@ -5,82 +5,22 @@
##
## This enables ASCIIMathJAX, and is used by js_textbox
<script type="text/x-mathjax-config">
// (function () {
var QUEUE = MathJax.Hub.queue; // shorthand for the queue
var math = null;
var jaxset = {}; // associative array of the element jaxs for the math output.
var mmlset = {}; // associative array of mathml from each jax
// constructs mathML of the specified jax element
function toMathML(jax,callback) {
var mml;
try {
mml = jax.root.toMathML("");
} catch(err) {
if (!err.restart) {throw err} // an actual error
return MathJax.Callback.After([toMathML,jax,callback],err.restart);
}
MathJax.Callback(callback)(mml);
}
// function to queue in MathJax to get put the MathML expression in in the right document element
function UpdateMathML(jax,id) {
toMathML(jax,function (mml) {
// document.getElementById(id+'_dynamath').value=math.originalText+ "\n\n=>\n\n"+ mml;
delem = document.getElementById("input_" + id + "_dynamath");
if (delem) { delem.value=mml; };
mmlset[id] = mml;
})
}
MathJax.Hub.Config({
tex2jax: {
inlineMath: [
["\\(","\\)"],
['[mathjaxinline]','[/mathjaxinline]']
],
displayMath: [
["\\[","\\]"],
['[mathjax]','[/mathjax]']
]
}
});
//
// The onchange event handler that typesets the
// math entered by the user
//
window.UpdateMath = function (Am,id) {
QUEUE.Push(["Text",jaxset[id],Am]);
QUEUE.Push(UpdateMathML(jaxset[id],id));
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {
inlineMath: [
["\\(","\\)"],
['[mathjaxinline]','[/mathjaxinline]']
],
displayMath: [
["\\[","\\]"],
['[mathjax]','[/mathjax]']
]
}
});
</script>
// })();
function DoUpdateMath(inputId) {
var str = document.getElementById("input_"+inputId).value;
// make sure the input field is in the jaxset
if ($.inArray(inputId,jaxset) == -1){
//alert('missing '+inputId);
if (document.getElementById("display_" + inputId)){
MathJax.Hub.queue.Push(function () {
math = MathJax.Hub.getAllJax("display_" + inputId)[0];
if (math){
jaxset[inputId] = math;
}
});
};
}
UpdateMath(str,inputId)
}
</script>
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
MathJax extension libraries -->
<script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
<!-- This must appear after all mathjax-config blocks, so it is after the imports from the other templates.
It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of
MathJax extension libraries -->
<script type="text/javascript" src="/static/js/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
......@@ -44,17 +44,17 @@ task :default => [:pep8, :pylint, :test]
directory REPORT_DIR
directory LMS_REPORT_DIR
desc "Run pep8 on all of djangoapps"
task :pep8 => LMS_REPORT_DIR do
sh("pep8 --ignore=E501 lms/djangoapps | tee #{LMS_REPORT_DIR}/pep8.report")
desc "Run pep8 on all libraries"
task :pep8 => REPORT_DIR do
sh("pep8 --ignore=E501 lms/djangoapps common/lib/* | tee #{REPORT_DIR}/pep8.report")
end
desc "Run pylint on all of djangoapps"
task :pylint => LMS_REPORT_DIR do
ENV['PYTHONPATH'] = 'lms/djangoapps'
Dir["lms/djangoapps/*"].each do |app|
desc "Run pylint on all libraries"
task :pylint => REPORT_DIR do
Dir["lms/djangoapps/*", "common/lib/*"].each do |app|
ENV['PYTHONPATH'] = File.dirname(app)
app = File.basename(app)
sh("pylint -f parseable #{app} | tee #{LMS_REPORT_DIR}/#{app}.pylint.report")
sh("pylint --rcfile=.pylintrc -f parseable #{app} | tee #{REPORT_DIR}/#{app}.pylint.report")
end
end
......@@ -66,6 +66,7 @@ end
desc "Run all django tests on our djangoapps for the #{system}"
task task_name => report_dir do
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
ENV['NOSE_COVER_HTML_DIR'] = File.join(report_dir, "cover")
sh(django_admin(system, :test, 'test', *Dir["#{system}/djangoapps/*"].each))
end
task :test => task_name
......@@ -83,13 +84,13 @@ end
Dir["common/lib/*"].each do |lib|
task_name = "test_#{lib}"
report_dir = File.join(REPORT_DIR, task_name)
report_dir = File.join(REPORT_DIR, task_name.gsub('/', '_'))
directory report_dir
desc "Run tests for common lib #{lib}"
task task_name do
task task_name => report_dir do
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
sh("nosetests #{lib}")
sh("nosetests #{lib} --cover-erase --with-xunit --with-xcoverage --cover-html --cover-inclusive --cover-package #{File.basename(lib)} --cover-html-dir #{File.join(report_dir, "cover")}")
end
task :test => task_name
end
......
......@@ -25,3 +25,6 @@ newrelic
glob2
pymongo
-e common/lib/xmodule
django_nose
nosexcover
rednose
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