Commit ced91369 by Calen Pennington

Merge remote-tracking branch 'origin/master' into cpennington/cms-xml-processing

parents 83b3d51f 968ff732
This branch (re-)adds dynamic math and symbolicresponse.
Test cases included.
see doc/ for documentation.
......@@ -12,13 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
log = logging.getLogger("mitx.common.lib.mitxmako")
from django.template import Context
from django.http import HttpResponse
from . import middleware
from django.conf import settings
def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_instance = Context(dictionary)
# add dictionary to context_instance
......@@ -27,8 +30,11 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_dictionary = {}
context_instance['settings'] = settings
context_instance['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
for d in middleware.requestcontext:
context_dictionary.update(d)
# In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None:
for d in middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
if context:
......
......@@ -108,7 +108,9 @@ class Module(XModule):
d = self.get_score()
score = d['score']
total = d['total']
return Progress(score, total)
if total > 0:
return Progress(score, total)
return None
def get_html(self):
......@@ -305,7 +307,7 @@ class Module(XModule):
after = self.get_progress()
d.update({
'progress_changed' : after != before,
'progress' : after.ternary_str(),
'progress_status' : Progress.to_js_status_str(after),
})
return json.dumps(d, cls=ComplexEncoder)
......
'''
Progress class for modules. Represents where a student is in a module.
'''
Useful things to know:
- Use Progress.to_js_status_str() to convert a progress into a simple
status string to pass to js.
- Use Progress.to_js_detail_str() to convert a progress into a more detailed
string to pass to js.
In particular, these functions have a canonical handing of None.
For most subclassing needs, you should only need to reimplement
frac() and __str__().
'''
from collections import namedtuple
import numbers
......@@ -124,3 +135,26 @@ class Progress(object):
(n, d) = a.frac()
(n2, d2) = b.frac()
return Progress(n + n2, d + d2)
@staticmethod
def to_js_status_str(progress):
'''
Return the "status string" version of the passed Progress
object that should be passed to js. Use this function when
sending Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return progress.ternary_str()
@staticmethod
def to_js_detail_str(progress):
'''
Return the "detail string" version of the passed Progress
object that should be passed to js. Use this function when
passing Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return str(progress)
......@@ -72,8 +72,8 @@ class Module(XModule):
for contents, title, progress in zip(self.contents, titles, progresses):
contents['title'] = title
contents['progress_str'] = str(progress) if progress is not None else ""
contents['progress_stat'] = progress.ternary_str() if progress is not None else ""
contents['progress_status'] = Progress.to_js_status_str(progress)
contents['progress_detail'] = Progress.to_js_detail_str(progress)
for (content, element_class) in zip(self.contents, child_classes):
new_class = 'other'
......
......@@ -568,6 +568,22 @@ class ProgressTest(unittest.TestCase):
self.assertEqual(self.half_done.ternary_str(), "in_progress")
self.assertEqual(self.done.ternary_str(), "done")
def test_to_js_status(self):
'''Test the Progress.to_js_status_str() method'''
self.assertEqual(Progress.to_js_status_str(self.not_started), "none")
self.assertEqual(Progress.to_js_status_str(self.half_done), "in_progress")
self.assertEqual(Progress.to_js_status_str(self.done), "done")
self.assertEqual(Progress.to_js_status_str(None), "NA")
def test_to_js_detail_str(self):
'''Test the Progress.to_js_detail_str() method'''
f = Progress.to_js_detail_str
for p in (self.not_started, self.half_done, self.done):
self.assertEqual(f(p), str(p))
# But None should be encoded as NA
self.assertEqual(f(None), "NA")
def test_add(self):
'''Test the Progress.add_counts() method'''
p = Progress(0, 2)
......
......@@ -4,6 +4,7 @@ import logging
from lxml import etree
from x_module import XModule, XModuleDescriptor
from progress import Progress
log = logging.getLogger("mitx.courseware.modules")
......@@ -15,17 +16,32 @@ class Module(XModule):
video_time = 0
def handle_ajax(self, dispatch, get):
'''
Handle ajax calls to this video.
TODO (vshnayder): This is not being called right now, so the position
is not being saved.
'''
log.debug(u"GET {0}".format(get))
log.debug(u"DISPATCH {0}".format(dispatch))
if dispatch == 'goto_position':
self.position = int(float(get['position']))
log.debug(u"NEW POSITION {0}".format(self.position))
log.info(u"NEW POSITION {0}".format(self.position))
return json.dumps({'success':True})
raise Http404()
def get_progress(self):
''' TODO (vshnayder): Get and save duration of youtube video, then return
fraction watched.
(Be careful to notice when video link changes and update)
For now, we have no way of knowing if the video has even been watched, so
just return None.
'''
return None
def get_state(self):
log.debug(u"STATE POSITION {0}".format(self.position))
return json.dumps({ 'position':self.position })
return json.dumps({ 'position': self.position })
@classmethod
def get_xml_tags(c):
......@@ -41,15 +57,16 @@ class Module(XModule):
'id': self.item_id,
'position': self.position,
'name': self.name,
'annotations': self.annotations
'annotations': self.annotations,
})
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml)
xmltree = etree.fromstring(xml)
self.youtube = xmltree.get('youtube')
self.name = xmltree.get('name')
self.position = 0
if state is not None:
state = json.loads(state)
if 'position' in state:
......
Common test infrastructure for LMS + CMS
===========================
data/ has some test course data.
Once the course validation is separated from django, we should have scripts here that checks that a course consists only of xml that we understand.
This is a realistic course, with many different module types and a lot of structure. It is based on 6.002x.
This is a simple, but non-trivial, course using multiple module types and some nested structure.
This is a very very simple course, useful for initial debugging of processing code.
<course name="Toy Course" graceperiod="1 day 5 hours 59 minutes 59 seconds" showanswer="always" rerandomize="never">
<chapter name="Overview">
<section format="Video" name="Welcome">
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
</section>
<section format="Lecture Sequence" name="System Usage Sequence">
<html id="Lab2A" filename="Lab2A.html"/>
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
</section>
</chapter>
</course>
......@@ -10,46 +10,98 @@ from courseware.content_parser import course_file
import courseware.module_render
import xmodule
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
def check_names(user, course):
'''
Complain if any problems have alphanumeric names.
TODO (vshnayder): there are some in 6.002x that don't. Is that actually a problem?
'''
all_ok = True
print "Confirming all problems have alphanumeric names"
for problem in course.xpath('//problem'):
filename = problem.get('filename')
if not filename.isalnum():
print "==============> Invalid (non-alphanumeric) filename", filename
all_ok = False
return all_ok
def check_rendering(user, course):
'''Check that all modules render'''
all_ok = True
print "Confirming all modules render. Nothing should print during this step. "
for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'):
module_class = xmodule.modx_modules[module.tag]
# TODO: Abstract this out in render_module.py
try:
module_class(etree.tostring(module),
module.get('id'),
ajax_url='',
state=None,
track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','type':'video'})
except Exception as ex:
print "==============> Error in ", etree.tostring(module)
print ""
print ex
all_ok = False
print "Module render check finished"
return all_ok
def check_sections(user, course):
all_ok = True
sections_dir = settings.DATA_DIR + "/sections"
print "Checking that all sections exist and parse properly"
if os.path.exists(sections_dir):
print "Checking all section includes are valid XML"
for f in os.listdir(sections_dir):
sectionfile = sections_dir + '/' + f
#print sectionfile
# skip non-xml files:
if not sectionfile.endswith('xml'):
continue
try:
etree.parse(sectionfile)
except Exception as ex:
print "================> Error parsing ", sectionfile
print ex
all_ok = False
print "checked all sections"
else:
print "Skipping check of include files -- no section includes dir ("+sections_dir+")"
return all_ok
class Command(BaseCommand):
help = "Does basic validity tests on course.xml."
def handle(self, *args, **options):
check = True
all_ok = True
# TODO (vshnayder): create dummy user objects. Anon, authenticated, staff.
# Check that everything works for each.
# The objects probably shouldn't be actual django users to avoid unneeded
# dependency on django.
# TODO: use args as list of files to check. Fix loading to work for other files.
sample_user = User.objects.all()[0]
print "Attempting to load courseware"
course = course_file(sample_user)
print "Confirming all problems have alphanumeric names"
for problem in course.xpath('//problem'):
filename = problem.get('filename')
if not filename.isalnum():
print "==============> Invalid (non-alphanumeric) filename", filename
check = False
print "Confirming all modules render. Nothing should print during this step. "
for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'):
module_class = xmodule.modx_modules[module.tag]
# TODO: Abstract this out in render_module.py
try:
module_class(etree.tostring(module),
module.get('id'),
ajax_url='',
state=None,
track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','type':'video'})
except:
print "==============> Error in ", etree.tostring(module)
check = False
print "Module render check finished"
sections_dir = settings.DATA_DIR+"sections"
if os.path.exists(sections_dir):
print "Checking all section includes are valid XML"
for f in os.listdir(sections_dir):
print f
etree.parse(sections_dir+'/'+f)
else:
print "Skipping check of include files -- no section includes dir ("+sections_dir+")"
to_run = [check_names,
# TODO (vshnayder) : make check_rendering work (use module_render.py),
# turn it on
# check_rendering,
check_sections,
]
for check in to_run:
all_ok = check(sample_user, course) and all_ok
# TODO: print "Checking course properly annotated with preprocess.py"
if check:
if all_ok:
print 'Courseware passes all checks!'
else:
print "Courseware fails some checks"
......@@ -199,7 +199,7 @@ def get_module(user, request, module_xml, student_module_cache, position=None):
etree.tostring(module_xml),
module_id,
state=state)
# If StudentModule for this instance wasn't already in the database,
# and this isn't a guest user, create it.
if not smod and user.is_authenticated():
......
......@@ -19,7 +19,7 @@ class @Problem
update_progress: (response) =>
if response.progress_changed
@element.attr progress: response.progress
@element.attr progress: response.progress_status
@element.trigger('progressChanged')
render: (content) ->
......
......@@ -20,8 +20,16 @@ class @Sequence
$('.problems-wrapper').bind 'progressChanged', @updateProgress
mergeProgress: (p1, p2) ->
# if either is "NA", return the other one
if p1 == "NA"
return p2
if p2 == "NA"
return p1
# Both real progresses
if p1 == "done" and p2 == "done"
return "done"
# not done, so if any progress on either, in_progress
w1 = p1 == "done" or p1 == "in_progress"
w2 = p2 == "done" or p2 == "in_progress"
......@@ -31,7 +39,7 @@ class @Sequence
return "none"
updateProgress: =>
new_progress = "none"
new_progress = "NA"
_this = this
$('.problems-wrapper').each (index) ->
progress = $(this).attr 'progress'
......@@ -41,6 +49,7 @@ class @Sequence
@setProgress(new_progress, @link_for(@position))
setProgress: (progress, element) ->
# If progress is "NA", don't add any css class
element.removeClass('progress-none')
.removeClass('progress-some')
.removeClass('progress-done')
......@@ -53,10 +62,12 @@ class @Sequence
$.each @elements, (index, item) =>
link = $('<a>').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1
title = $('<p>').html(item.title)
# TODO: add item.progress_str either to the title or somewhere else.
# Make sure it gets updated after ajax calls
# TODO (vshnayder): add item.progress_detail either to the title or somewhere else.
# Make sure it gets updated after ajax calls.
# implementation note: will need to figure out how to handle combining detail
# statuses of multiple modules in js.
list_item = $('<li>').append(link.append(title))
@setProgress item.progress_stat, link
@setProgress item.progress_status, link
@$('#sequence-list').append list_item
......
......@@ -66,6 +66,26 @@ nav.sequence-nav {
@include transition(all, .4s, $ease-in-out-quad);
width: 100%;
&.progress {
border-bottom-style: solid;
border-bottom-width: 4px;
}
&.progress-none {
@extend .progress;
border-bottom-color: red;
}
&.progress-some {
@extend .progress;
border-bottom-color: yellow;
}
&.progress-done {
@extend .progress;
border-bottom-color: green;
}
//video
&.seq_video_inactive {
@extend .inactive;
......
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