Commit f28cba2a by David Ormsbee

Merge pull request #88 from MITx/cpennington/courseware_tests

Move courseware tests into the common/xmodule library
parents d7957424 a83fdc71
......@@ -21,8 +21,6 @@ from lxml import etree
from lxml.etree import Element
from xml.sax.saxutils import escape, unescape
from mako.template import Template
from util import contextualize_text
import inputtypes
......
......@@ -27,8 +27,6 @@ import shlex # for splitting quoted strings
from lxml import etree
from mitxmako.shortcuts import render_to_string
def get_input_xml_tags():
''' Eventually, this will be for all registered input types '''
return SimpleInput.get_xml_tags()
......@@ -54,7 +52,7 @@ class SimpleInput():# XModule
return ['capa_input', 'capa_transform']
def get_html(self):
return self.xml_tags[self.tag](self.xml, self.value, self.status, self.msg)
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'):
self.xml = xml
......@@ -144,7 +142,7 @@ def register_render_function(fn, names=None, cls=SimpleInput):
#-----------------------------------------------------------------------------
@register_render_function
def optioninput(element, value, status, msg=''):
def optioninput(element, value, status, render_template, msg=''):
'''
Select option input type.
......@@ -171,12 +169,12 @@ def optioninput(element, value, status, msg=''):
'options':osetdict,
}
html=render_to_string("optioninput.html", context)
html = render_template("optioninput.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
def choicegroup(element, value, status, msg=''):
def choicegroup(element, value, status, render_template, msg=''):
'''
Radio button inputs: multiple choice or true/false
......@@ -199,11 +197,11 @@ def choicegroup(element, value, status, msg=''):
ctext += choice.text # TODO: fix order?
choices[choice.get("name")] = ctext
context={'id':eid, 'value':value, 'state':status, 'type':type, 'choices':choices}
html=render_to_string("choicegroup.html", context)
html = render_template("choicegroup.html", context)
return etree.XML(html)
@register_render_function
def textline(element, value, state, msg=""):
def textline(element, value, state, render_template, msg=""):
'''
Simple text line input, with optional size specification.
'''
......@@ -213,13 +211,13 @@ def textline(element, value, state, 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}
html=render_to_string("textinput.html", context)
html = render_template("textinput.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
def textline_dynamath(element, value, status, msg=''):
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).
'''
......@@ -237,13 +235,13 @@ def textline_dynamath(element, value, status, msg=''):
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size,
'msg':msg,
}
html=render_to_string("textinput_dynamath.html", context)
html = render_template("textinput_dynamath.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
## TODO: Make a wrapper for <codeinput>
@register_render_function
def textbox(element, value, status, msg=''):
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.
......@@ -261,12 +259,12 @@ def textbox(element, value, status, msg=''):
'mode':mode, 'linenumbers':linenumbers,
'rows':rows, 'cols':cols,
}
html=render_to_string("textbox.html", context)
html = render_template("textbox.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
def schematic(element, value, status, msg=''):
def schematic(element, value, status, render_template, msg=''):
eid = element.get('id')
height = element.get('height')
width = element.get('width')
......@@ -285,13 +283,13 @@ def schematic(element, value, status, msg=''):
'analyses':analyses,
'submit_analyses':submit_analyses,
}
html=render_to_string("schematicinput.html", context)
html = render_template("schematicinput.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
### TODO: Move out of inputtypes
@register_render_function
def math(element, value, status, msg=''):
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.
......@@ -316,7 +314,7 @@ def math(element, value, status, msg=''):
# mathstr = mathstr.replace('\\displaystyle','')
#else:
# isinline = True
# html=render_to_string("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
# html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr,element.tail)
xhtml = etree.XML(html)
......@@ -326,7 +324,7 @@ def math(element, value, status, msg=''):
#-----------------------------------------------------------------------------
@register_render_function
def solution(element, value, status, msg=''):
def solution(element, value, status, render_template, msg=''):
'''
This is not really an input type. It is just a <span>...</span> which is given an ID,
that is used for displaying an extended answer (a problem "solution") after "show answers"
......@@ -341,13 +339,13 @@ def solution(element, value, status, msg=''):
'size': size,
'msg':msg,
}
html=render_to_string("solutionspan.html", context)
html = render_template("solutionspan.html", context)
return etree.XML(html)
#-----------------------------------------------------------------------------
@register_render_function
def imageinput(element, value, status, msg=''):
def imageinput(element, value, status, render_template, msg=''):
'''
Clickable image as an input field. Element should specify the image source, height, and width, eg
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="388" height="560" />
......@@ -378,5 +376,5 @@ def imageinput(element, value, status, msg=''):
'state' : status, # to change
'msg': msg, # to change
}
html=render_to_string("imageinput.html", context)
html = render_template("imageinput.html", context)
return etree.XML(html)
......@@ -256,8 +256,7 @@ def sympy_check2():
self.expect = xml.get('expect') or xml.get('answer')
self.myid = xml.get('id')
if settings.DEBUG:
log.info('answer_ids=%s' % self.answer_ids)
log.debug('answer_ids=%s' % self.answer_ids)
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
self.code = None
......@@ -271,7 +270,7 @@ def sympy_check2():
# ie the comparison function is defined in the <script>...</script> stanza instead
cfn = xml.get('cfn')
if cfn:
if settings.DEBUG: log.info("cfn = %s" % cfn)
log.debug("cfn = %s" % cfn)
if cfn in context:
self.code = context[cfn]
else:
......@@ -346,7 +345,7 @@ def sympy_check2():
# this is an interface to the Tutor2 check functions
fn = self.code
ret = None
if settings.DEBUG: log.info(" submission = %s" % submission)
log.debug(" submission = %s" % submission)
try:
answer_given = submission[0] if (len(idset)==1) else submission
# handle variable number of arguments in check function, for backwards compatibility
......@@ -358,9 +357,8 @@ def sympy_check2():
for argname in argspec.args[nargs:]:
kwargs[argname] = self.context[argname] if argname in self.context else None
if settings.DEBUG:
log.debug('[courseware.capa.responsetypes.customresponse] answer_given=%s' % answer_given)
log.info('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
log.debug('[customresponse] answer_given=%s' % answer_given)
log.debug('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
ret = fn(*args[:nargs],**kwargs)
except Exception,err:
......@@ -368,7 +366,7 @@ def sympy_check2():
# print "context = ",self.context
log.error(traceback.format_exc())
raise Exception,"oops in customresponse (cfn) error %s" % err
if settings.DEBUG: log.info("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
log.debug("[courseware.capa.responsetypes.customresponse.get_score] ret = %s" % ret)
if type(ret)==dict:
correct = ['correct']*len(idset) if ret['ok'] else ['incorrect']*len(idset)
msg = ret['msg']
......
......@@ -10,9 +10,6 @@ import StringIO
from datetime import timedelta
from lxml import etree
## TODO: Abstract out from Django
from mitxmako.shortcuts import render_to_string
from x_module import XModule, XModuleDescriptor
from capa.capa_problem import LoncapaProblem, StudentInputError
log = logging.getLogger("mitx.courseware")
......@@ -73,9 +70,9 @@ class Module(XModule):
return self.lcp.get_max_score()
def get_html(self):
return render_to_string('problem_ajax.html',
{'id':self.item_id,
'ajax_url':self.ajax_url,
return self.system.render_template('problem_ajax.html', {
'id': self.item_id,
'ajax_url': self.ajax_url,
})
def get_problem_html(self, encapsulate=True):
......@@ -139,7 +136,7 @@ class Module(XModule):
'explain': explain,
}
html=render_to_string('problem.html', context)
html = self.system.render_template('problem.html', context)
if encapsulate:
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(id=self.item_id,ajax_url=self.ajax_url)+html+"</div>"
......
import abc
import logging
from django.conf import settings
from collections import namedtuple
log = logging.getLogger("mitx.courseware")
......@@ -11,6 +9,34 @@ log = logging.getLogger("mitx.courseware")
# Section either indicates the name of the problem or the name of the section
Score = namedtuple("Score", "earned possible graded section")
def aggregate_scores(scores, section_name="summary"):
"""
scores: A list of Score objects
returns: A tuple (all_total, graded_total).
all_total: A Score representing the total score summed over all input scores
graded_total: A Score representing the score summed over all graded input scores
"""
total_correct_graded = sum(score.earned for score in scores if score.graded)
total_possible_graded = sum(score.possible for score in scores if score.graded)
total_correct = sum(score.earned for score in scores)
total_possible = sum(score.possible for score in scores)
#regardless of whether or not it is graded
all_total = Score(total_correct,
total_possible,
False,
section_name)
#selecting only graded things
graded_total = Score(total_correct_graded,
total_possible_graded,
True,
section_name)
return all_total, graded_total
def grader_from_conf(conf):
"""
This creates a CourseGrader from a configuration (such as in course_settings.py).
......@@ -162,18 +188,6 @@ class SingleSectionGrader(CourseGrader):
percent = 0.0
detail = "{name} - 0% (?/?)".format(name = self.name)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(50, 100)
points_earned = random.randrange(40, points_possible)
percent = points_earned / float(points_possible)
detail = "{name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format( name = self.name,
percent = percent,
earned = float(points_earned),
possible = float(points_possible))
breakdown = [{'percent': percent, 'label': self.short_label, 'detail': detail, 'category': self.category, 'prominent': True}]
return {'percent' : percent,
......@@ -244,17 +258,6 @@ class AssignmentFormatGrader(CourseGrader):
percentage = 0
summary = "{section_type} {index} Unreleased - 0% (?/?)".format(index = i+1, section_type = self.section_type)
if settings.GENERATE_PROFILE_SCORES:
points_possible = random.randrange(10, 50)
points_earned = random.randrange(5, points_possible)
percentage = points_earned / float(points_possible)
summary = "{section_type} {index} - {name} - {percent:.0%} ({earned:.3n}/{possible:.3n})".format(index = i+1,
section_type = self.section_type,
name = "Randomly Generated",
percent = percentage,
earned = float(points_earned),
possible = float(points_possible) )
short_label = "{short_label} {index:02d}".format(index = i+1, short_label = self.short_label)
breakdown.append( {'percent': percentage, 'label': short_label, 'detail': summary, 'category': self.category} )
......
import json
import logging
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule, XModuleDescriptor
from lxml import etree
......@@ -34,8 +32,7 @@ class Module(XModule):
except: # For backwards compatibility. TODO: Remove
if self.DEBUG:
log.info('[courseware.modules.html_module] filename=%s' % self.filename)
#return render_to_string(self.filename, {'id': self.item_id})
return render_to_string(self.filename, {'id': self.item_id},namespace='course')
return self.system.render_template(self.filename, {'id': self.item_id}, namespace='course')
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
......
......@@ -2,8 +2,6 @@ import json
from lxml import etree
from mitxmako.shortcuts import render_to_string
from x_module import XModule, XModuleDescriptor
# HACK: This shouldn't be hard-coded to two types
......@@ -77,9 +75,9 @@ class Module(XModule):
'tag':self.xmltree.tag}
if self.xmltree.tag in ['sequential', 'videosequence']:
self.content=render_to_string('seq_module.html',params)
self.content = self.system.render_template('seq_module.html', params)
if self.xmltree.tag == 'tab':
self.content=render_to_string('tab_module.html',params)
self.content = self.system.render_template('tab_module.html', params)
self.rendered = True
def __init__(self, system, xml, item_id, state=None):
......
import json
from mitxmako.shortcuts import render_to_string
from x_module import XModule, XModuleDescriptor
from lxml import etree
......@@ -24,4 +22,4 @@ class Module(XModule):
xmltree = etree.fromstring(xml)
filename = xmltree[0].text
params = dict(xmltree.items())
self.html = render_to_string(filename, params, namespace = 'custom_tags')
self.html = self.system.render_template(filename, params, namespace = 'custom_tags')
......@@ -13,9 +13,8 @@ import numpy
import xmodule
import capa.calc as calc
import capa.capa_problem as lcp
import courseware.graders as graders
from courseware.graders import Score, CourseGrader, WeightedSubsectionsGrader, SingleSectionGrader, AssignmentFormatGrader
from courseware.grades import aggregate_scores
from xmodule import graders
from xmodule.graders import Score, aggregate_scores
from nose.plugins.skip import SkipTest
class I4xSystem(object):
......
import json
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule, XModuleDescriptor
from lxml import etree
......@@ -19,7 +17,9 @@ class Module(XModule):
return ["vertical", "problemset"]
def get_html(self):
return render_to_string('vert_module.html',{'items':self.contents})
return self.system.render_template('vert_module.html', {
'items': self.contents
})
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
......
......@@ -3,8 +3,6 @@ import logging
from lxml import etree
from mitxmako.shortcuts import render_to_response, render_to_string
from x_module import XModule, XModuleDescriptor
log = logging.getLogger("mitx.courseware.modules")
......@@ -38,11 +36,13 @@ class Module(XModule):
return self.youtube
def get_html(self):
return render_to_string('video.html',{'streams':self.video_list(),
'id':self.item_id,
'position':self.position,
'name':self.name,
'annotations':self.annotations})
return self.system.render_template('video.html', {
'streams': self.video_list(),
'id': self.item_id,
'position': self.position,
'name': self.name,
'annotations': self.annotations
})
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
......
from lxml import etree
import courseware.progress
def dummy_track(event_type, event):
pass
......@@ -71,11 +69,6 @@ class XModule(object):
### Functions used in the LMS
def get_completion(self):
''' This is mostly unimplemented.
It gives a progress indication -- e.g. 30 minutes of 1.5 hours watched. 3 of 5 problems done, etc. '''
return courseware.progress.completion()
def get_state(self):
''' State of the object, as stored in the database
'''
......
......@@ -25,8 +25,8 @@ import types
from django.conf import settings
from courseware import global_course_settings
from courseware import graders
from courseware.graders import Score
from xmodule import graders
from xmodule.graders import Score
from models import StudentModule
import courseware.content_parser as content_parser
import xmodule
......@@ -116,7 +116,7 @@ def grade_sheet(student,coursename=None):
graded = False
scores.append( Score(correct,total, graded, p.get("name")) )
section_total, graded_total = aggregate_scores(scores, s.get("name"))
section_total, graded_total = graders.aggregate_scores(scores, s.get("name"))
#Add the graded total to totaled_scores
format = s.get('format', "")
subtitle = s.get('subtitle', format)
......@@ -146,27 +146,6 @@ def grade_sheet(student,coursename=None):
return {'courseware_summary' : chapters,
'grade_summary' : grade_summary}
def aggregate_scores(scores, section_name = "summary"):
total_correct_graded = sum(score.earned for score in scores if score.graded)
total_possible_graded = sum(score.possible for score in scores if score.graded)
total_correct = sum(score.earned for score in scores)
total_possible = sum(score.possible for score in scores)
#regardless of whether or not it is graded
all_total = Score(total_correct,
total_possible,
False,
section_name)
#selecting only graded things
graded_total = Score(total_correct_graded,
total_possible_graded,
True,
section_name)
return all_total, graded_total
def get_score(user, problem, cache, coursename=None):
## HACK: assumes max score is fixed per problem
id = problem.get('id')
......
......@@ -27,7 +27,7 @@ class I4xSystem(object):
of the courseware (e.g. import into other types of courseware, LMS,
or if we want to have a sandbox server for user-contributed content)
'''
def __init__(self, ajax_url, track_function, render_function, filestore=None):
def __init__(self, ajax_url, track_function, render_function, render_template, filestore=None):
self.ajax_url = ajax_url
self.track_function = track_function
if not filestore:
......@@ -37,6 +37,7 @@ class I4xSystem(object):
if settings.DEBUG:
log.info("[courseware.module_render.I4xSystem] filestore path = %s" % filestore)
self.render_function = render_function
self.render_template = render_template
self.exception404 = Http404
self.DEBUG = settings.DEBUG
......@@ -117,6 +118,7 @@ def get_module(user, request, xml_module, module_object_preload, position=None):
system = I4xSystem(track_function = make_track_function(request),
render_function = lambda x: render_x_module(user, request, x, module_object_preload, position),
render_template = render_to_string,
ajax_url = ajax_url,
filestore = OSFS(data_root),
)
......@@ -225,6 +227,7 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
# Create the module
system = I4xSystem(track_function = make_track_function(request),
render_function = None,
render_template = render_to_string,
ajax_url = ajax_url,
filestore = OSFS(data_root),
)
......
......@@ -179,6 +179,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
# Create the module (instance of capa_module.Module)
system = I4xSystem(track_function = make_track_function(request),
render_function = None,
render_template = render_to_string,
ajax_url = ajax_url,
filestore = OSFS(settings.DATA_DIR + xp),
#role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
......
......@@ -58,10 +58,31 @@ task :pylint => LMS_REPORT_DIR do
end
end
desc "Run all django tests on our djangoapps"
task :test => LMS_REPORT_DIR do
ENV['NOSE_XUNIT_FILE'] = File.join(LMS_REPORT_DIR, "nosetests.xml")
[:lms].each do |system|
task_name = "test_#{system}"
report_dir = File.join(REPORT_DIR, task_name)
directory report_dir
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")
sh(django_admin(:lms, :test, 'test', *Dir['lms/djangoapps'].each))
end
task :test => task_name
end
Dir["common/lib/*"].each do |lib|
task_name = "test_#{lib}"
report_dir = File.join(REPORT_DIR, task_name)
directory report_dir
desc "Run tests for common lib #{lib}"
task task_name do
ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
sh("nosetests #{lib}")
end
task :test => task_name
end
desc <<-desc
......
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