Commit 03111161 by Victor Shnayder

Add a chemicalequationinput with live preview

- architecturally slightly questionable: the preview ajax calls goes to an LMS view instead of an input type specific one.  This needs to be fixed during the grand capa re-org, but there isn't time to do it right now.
- also, I kind of like having a generic turn-a-formula-into-a-preview service available
parent 9bc433d2
......@@ -55,7 +55,8 @@ entry_types = ['textline',
'radiogroup',
'checkboxgroup',
'filesubmission',
'javascriptinput',]
'javascriptinput',
'chemicalequationinput']
# extra things displayed after "show answers" is pressed
solution_types = ['solution']
......
......@@ -125,8 +125,13 @@ def _merge_children(tree, tags):
(group 1 2 3 4)
It has to handle this recursively:
(group 1 (group 2 (group 3 (group 4))))
We do the cleanup of converting from the latter to the former (as a
We do the cleanup of converting from the latter to the former.
'''
if tree is None:
# There was a problem--shouldn't have empty trees (NOTE: see this with input e.g. 'H2O(', or 'Xe+').
# Haven't grokked the code to tell if this is indeed the right thing to do.
raise ParseException("Shouldn't have empty trees")
if type(tree) == str:
return tree
......@@ -195,14 +200,52 @@ def _render_to_html(tree):
return children.replace(' ', '')
def render_to_html(s):
''' render a string to html '''
status = _render_to_html(_get_final_tree(s))
return status
def render_to_html(eq):
'''
Render a chemical equation string to html.
Renders each molecule separately, and returns invalid input wrapped in a <span>.
'''
def err(s):
"Render as an error span"
return '<span class="inline-error inline">{0}</span>'.format(s)
def render_arrow(arrow):
"""Turn text arrows into pretty ones"""
if arrow == '->':
return u'\u2192'
if arrow == '<->':
return u'\u2194'
return arrow
def render_expression(ex):
"""
Render a chemical expression--no arrows.
"""
try:
return _render_to_html(_get_final_tree(ex))
except ParseException:
return err(ex)
def spanify(s):
return u'<span class="math">{0}</span>'.format(s)
left, arrow, right = split_on_arrow(eq)
if arrow == '':
# only one side
return spanify(render_expression(left))
return spanify(render_expression(left) + render_arrow(arrow) + render_expression(right))
def _get_final_tree(s):
''' return final tree after merge and clean '''
'''
Return final tree after merge and clean.
Raises pyparsing.ParseException if s is invalid.
'''
tokenized = tokenizer.parseString(s)
parsed = parser.parse(tokenized)
merged = _merge_children(parsed, {'S','group'})
......@@ -227,14 +270,14 @@ def _check_equality(tuple1, tuple2):
def compare_chemical_expression(s1, s2, ignore_state=False):
''' It does comparison between two equations.
''' It does comparison between two expressions.
It uses divide_chemical_expression and check if division is 1
'''
return divide_chemical_expression(s1, s2, ignore_state) == 1
def divide_chemical_expression(s1, s2, ignore_state=False):
'''Compare two chemical equations for equivalence up to a multiplicative factor:
'''Compare two chemical expressions for equivalence up to a multiplicative factor:
- If they are not the same chemicals, returns False.
- If they are the same, "divide" s1 by s2 to returns a factor x such that s1 / s2 == x as a Fraction object.
......@@ -248,7 +291,7 @@ def divide_chemical_expression(s1, s2, ignore_state=False):
Implementation sketch:
- extract factors and phases to standalone lists,
- compare equations without factors and phases,
- compare expressions without factors and phases,
- divide lists of factors for each other and check
for equality of every element in list,
- return result of factor division
......@@ -294,7 +337,7 @@ def divide_chemical_expression(s1, s2, ignore_state=False):
treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases'] = zip(
*sorted(zip(treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases'])))
# check if equations are correct without factors
# check if expressions are correct without factors
if not _check_equality(treedic['1 cleaned_mm_list'], treedic['2 cleaned_mm_list']):
return False
......@@ -312,9 +355,26 @@ def divide_chemical_expression(s1, s2, ignore_state=False):
return Fraction(treedic['1 factors'][0] / treedic['2 factors'][0])
def split_on_arrow(eq):
"""
Split a string on an arrow. Returns left, arrow, right. If there is no arrow, returns the
entire eq in left, and '' in arrow and right.
Return left, arrow, right.
"""
# order matters -- need to try <-> first
arrows = ('<->', '->')
for arrow in arrows:
left, a, right = eq.partition(arrow)
if a != '':
return left, a, right
return eq, '', ''
def chemical_equations_equal(eq1, eq2, exact=False):
"""
Check whether two chemical equations are the same.
Check whether two chemical equations are the same. (equations have arrows)
If exact is False, then they are considered equal if they differ by a
constant factor.
......@@ -333,19 +393,13 @@ def chemical_equations_equal(eq1, eq2, exact=False):
If there's a syntax error, we raise pyparsing.ParseException.
"""
# for now, we do a manual parse for the arrow.
arrows = ('<->', '->') # order matters -- need to try <-> first
def split_on_arrow(s):
"""Split a string on an arrow. Returns left, arrow, right, or raises ParseException if there isn't an arrow"""
for arrow in arrows:
left, a, right = s.partition(arrow)
if a != '':
return left, a, right
raise ParseException("Could not find arrow. Legal arrows: {0}".format(arrows))
left1, arrow1, right1 = split_on_arrow(eq1)
left2, arrow2, right2 = split_on_arrow(eq2)
if arrow1 == '' or arrow2 == '':
raise ParseException("Could not find arrow. Legal arrows: {0}".format(arrows))
# TODO: may want to be able to give student helpful feedback about why things didn't work.
if arrow1 != arrow2:
# arrows don't match
......
......@@ -708,3 +708,34 @@ def imageinput(element, value, status, render_template, msg=''):
return etree.XML(html)
_reg(imageinput)
#--------------------------------------------------------------------------------
class ChemicalEquationInput(InputTypeBase):
"""
An input type for entering chemical equations. Supports live preview.
Example:
<chemicalequationinput size="50"/>
options: size -- width of the textbox.
"""
template = "chemicalequationinput.html"
tags = ['chemicalequationinput']
def _get_render_context(self):
size = self.xml.get('size', '20')
context = {
'id': self.id,
'value': self.value,
'status': self.status,
'size': size,
'previewer': '/static/js/capa/chemical_equation_preview.js',
}
return context
register_input_class(ChemicalEquationInput)
......@@ -867,7 +867,7 @@ def sympy_check2():
</customresponse>"""}]
response_tag = 'customresponse'
allowed_inputfields = ['textline', 'textbox']
allowed_inputfields = ['textline', 'textbox', 'chemicalequationinput']
def setup_response(self):
xml = self.xml
......
<section id="chemicalequationinput_${id}" class="chemicalequationinput">
<div class="script_placeholder" data-src="${previewer}"/>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if size:
size="${size}"
% endif
/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p>
<div class="equation">
</div>
<p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
These files really should be in the capa module, but we don't have a way to load js from there at the moment. (TODO)
(function () {
var preview_div = $('.chemicalequationinput .equation');
$('.chemicalequationinput input').bind("input", function(eventObject) {
$.get("/preview/chemcalc/", {"formula" : this.value}, function(response) {
if (response.error) {
preview_div.html("<span class='error'>" + response.error + "</span>");
} else {
preview_div.html(response.preview);
}
});
});
}).call(this);
import hashlib
import json
import logging
import pyparsing
import sys
from django.conf import settings
......@@ -13,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt
from requests.auth import HTTPBasicAuth
from capa.xqueue_interface import XQueueInterface
from capa.chem import chemcalc
from courseware.access import has_access
from mitxmako.shortcuts import render_to_string
from models import StudentModule, StudentModuleCache
......@@ -471,3 +473,42 @@ def modx_dispatch(request, dispatch, location, course_id):
# Return whatever the module wanted to return to the client/caller
return HttpResponse(ajax_return)
def preview_chemcalc(request):
"""
Render an html preview of a chemical formula or equation. The fact that
this is here is a bit of hack. See the note in lms/urls.py about why it's
here. (Victor is to blame.)
request should be a GET, with a key 'formula' and value 'some formula string'.
Returns a json dictionary:
{
'preview' : 'the-preview-html' or ''
'error' : 'the-error' or ''
}
"""
if request.method != "GET":
raise Http404
result = {'preview': '',
'error': '' }
formula = request.GET.get('formula')
if formula is None:
result['error'] = "No formula specified."
return HttpResponse(json.dumps(result))
try:
result['preview'] = chemcalc.render_to_html(formula)
except pyparsing.ParseException as p:
result['error'] = "Couldn't parse formula: {0}".format(p)
except Exception:
# this is unexpected, so log
log.warning("Error while previewing chemical formula", exc_info=True)
result['error'] = "Error while rendering preview"
return HttpResponse(json.dumps(result))
......@@ -141,6 +141,16 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/modx/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.modx_dispatch',
name='modx_dispatch'),
# TODO (vshnayder): This is a hack. It creates a direct connection from
# the LMS to capa functionality, and really wants to go through the
# input types system so that previews can be context-specific.
# Unfortunately, we don't have time to think through the right way to do
# that (and implement it), and it's not a terrible thing to provide a
# generic chemican-equation rendering service.
url(r'^preview/chemcalc', 'courseware.module_render.preview_chemcalc',
name='preview_chemcalc'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$',
'courseware.module_render.xqueue_callback',
name='xqueue_callback'),
......
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