Merge branch 'feature/christina/metadata-ui' of github.com:edx/edx-platform into feature/christina/metadata-ui

This commit is contained in:
marco
2013-05-20 15:22:31 -04:00
270 changed files with 12444 additions and 2261 deletions

1
common/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
jasmine_test_runner.html

View File

@@ -14,9 +14,57 @@
from django.template import Context
from django.http import HttpResponse
import logging
from . import middleware
from django.conf import settings
from django.core.urlresolvers import reverse
log = logging.getLogger(__name__)
def marketing_link(name):
"""Returns the correct URL for a link to the marketing site
depending on if the marketing site is enabled
Since the marketing site is enabled by a setting, we have two
possible URLs for certain links. This function is to decides
which URL should be provided.
"""
# link_map maps URLs from the marketing site to the old equivalent on
# the Django site
link_map = settings.MKTG_URL_LINK_MAP
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in settings.MKTG_URLS:
# special case for when we only want the root marketing URL
if name == 'ROOT':
return settings.MKTG_URLS.get('ROOT')
return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name)
# only link to the old pages when the marketing site isn't on
elif not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in link_map:
return reverse(link_map[name])
else:
log.warning("Cannot find corresponding link for name: {name}".format(name=name))
return '#'
def marketing_link_context_processor(request):
"""
A django context processor to give templates access to marketing URLs
Returns a dict whose keys are the marketing link names usable with the
marketing_link method (e.g. 'ROOT', 'CONTACT', etc.) prefixed with
'MKTG_URL_' and whose values are the corresponding URLs as computed by the
marketing_link method.
"""
return dict(
[
("MKTG_URL_" + k, marketing_link(k))
for k in (
settings.MKTG_URL_LINK_MAP.viewkeys() |
settings.MKTG_URLS.viewkeys()
)
]
)
def render_to_string(template_name, dictionary, context=None, namespace='main'):
@@ -27,6 +75,7 @@ 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
context_instance['marketing_link'] = marketing_link
# In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None:

View File

@@ -14,6 +14,7 @@
from django.conf import settings
from mako.template import Template as MakoTemplate
from mitxmako.shortcuts import marketing_link
from mitxmako import middleware
@@ -37,7 +38,6 @@ class Template(MakoTemplate):
kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs)
def render(self, context_instance):
"""
This takes a render call with a context (from Django) and translates
@@ -55,5 +55,6 @@ class Template(MakoTemplate):
context_dictionary['settings'] = settings
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
context_dictionary['django_context'] = context_instance
context_dictionary['marketing_link'] = marketing_link
return super(Template, self).render_unicode(**context_dictionary)

View File

@@ -0,0 +1,27 @@
from django.test import TestCase
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from django.conf import settings
from mitxmako.shortcuts import marketing_link
from mock import patch
class ShortcutsTests(TestCase):
"""
Test the mitxmako shortcuts file
"""
@override_settings(MKTG_URLS={'ROOT': 'dummy-root', 'ABOUT': '/about-us'})
@override_settings(MKTG_URL_LINK_MAP={'ABOUT': 'login'})
def test_marketing_link(self):
# test marketing site on
with patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
expected_link = 'dummy-root/about-us'
link = marketing_link('ABOUT')
self.assertEquals(link, expected_link)
# test marketing site off
with patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
# we are using login because it is common across both cms and lms
expected_link = reverse('login')
link = marketing_link('ABOUT')
self.assertEquals(link, expected_link)

View File

@@ -0,0 +1,3 @@
"""
Stub for a Django app to report the status of various services
"""

View File

@@ -0,0 +1,25 @@
"""
Django Celery tasks for service status app
"""
import time
from dogapi import dog_stats_api
from djcelery import celery
@celery.task
@dog_stats_api.timed('status.service.celery.pong')
def delayed_ping(value, delay):
"""A simple tasks that replies to a message after a especified amount
of seconds.
"""
if value == 'ping':
result = 'pong'
else:
result = 'got: {0}'.format(value)
time.sleep(delay)
return result

View File

@@ -0,0 +1,47 @@
"""Test for async task service status"""
from django.utils import unittest
from django.test.client import Client
from django.core.urlresolvers import reverse
import json
class CeleryConfigTest(unittest.TestCase):
"""
Test that we can get a response from Celery
"""
def setUp(self):
"""
Create a django test client
"""
self.client = Client()
self.ping_url = reverse('status.service.celery.ping')
def test_ping(self):
"""
Try to ping celery.
"""
# Access the service status page, which starts a delayed
# asynchronous task
response = self.client.get(self.ping_url)
# HTTP response should be successful
self.assertEqual(response.status_code, 200)
# Expect to get a JSON-serialized dict with
# task and time information
result_dict = json.loads(response.content)
# Was it successful?
self.assertTrue(result_dict['success'])
# We should get a "pong" message back
self.assertEqual(result_dict['value'], "pong")
# We don't know the other dict values exactly,
# but we can assert that they take the right form
self.assertTrue(isinstance(result_dict['task_id'], unicode))
self.assertTrue(isinstance(result_dict['time'], float))
self.assertTrue(result_dict['time'] > 0.0)

View File

@@ -0,0 +1,15 @@
"""
Django URLs for service status app
"""
from django.conf.urls import patterns, url
urlpatterns = patterns(
'',
url(r'^$', 'service_status.views.index', name='status.service.index'),
url(r'^celery/$', 'service_status.views.celery_status',
name='status.service.celery.status'),
url(r'^celery/ping/$', 'service_status.views.celery_ping',
name='status.service.celery.ping'),
)

View File

@@ -0,0 +1,59 @@
"""
Django Views for service status app
"""
import json
import time
from django.http import HttpResponse
from dogapi import dog_stats_api
from service_status import tasks
from djcelery import celery
from celery.exceptions import TimeoutError
def index(_):
"""
An empty view
"""
return HttpResponse()
@dog_stats_api.timed('status.service.celery.status')
def celery_status(_):
"""
A view that returns Celery stats
"""
stats = celery.control.inspect().stats() or {}
return HttpResponse(json.dumps(stats, indent=4),
mimetype="application/json")
@dog_stats_api.timed('status.service.celery.ping')
def celery_ping(_):
"""
A Simple view that checks if Celery can process a simple task
"""
start = time.time()
result = tasks.delayed_ping.apply_async(('ping', 0.1))
task_id = result.id
# Wait until we get the result
try:
value = result.get(timeout=4.0)
success = True
except TimeoutError:
value = None
success = False
output = {
'success': success,
'task_id': task_id,
'value': value,
'time': time.time() - start,
}
return HttpResponse(json.dumps(output, indent=4),
mimetype="application/json")

View File

@@ -7,7 +7,7 @@ import string
import sys
import urllib
import uuid
import time
from django.conf import settings
from django.contrib.auth import logout, authenticate, login
@@ -20,9 +20,10 @@ from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.core.validators import validate_email, validate_slug, ValidationError
from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed, HttpResponseRedirect, Http404
from django.shortcuts import redirect
from django_future.csrf import ensure_csrf_cookie, csrf_exempt
from django.utils.http import cookie_date
from mitxmako.shortcuts import render_to_response, render_to_string
from bs4 import BeautifulSoup
@@ -212,6 +213,36 @@ def _cert_info(user, course, cert_status):
return d
@ensure_csrf_cookie
def signin_user(request):
"""
This view will display the non-modal login form
"""
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
context = {
'course_id': request.GET.get('course_id'),
'enrollment_action': request.GET.get('enrollment_action')
}
return render_to_response('login.html', context)
@ensure_csrf_cookie
def register_user(request):
"""
This view will display the non-modal registration form
"""
if request.user.is_authenticated():
return redirect(reverse('dashboard'))
context = {
'course_id': request.GET.get('course_id'),
'enrollment_action': request.GET.get('enrollment_action')
}
return render_to_response('register.html', context)
@login_required
@ensure_csrf_cookie
def dashboard(request):
@@ -250,7 +281,7 @@ def dashboard(request):
exam_registrations = {course.id: exam_registration_info(request.user, course) for course in courses}
# Get the 3 most recent news
top_news = _get_news(top=3)
top_news = _get_news(top=3) if not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False) else None
context = {'courses': courses,
'message': message,
@@ -275,35 +306,47 @@ def try_change_enrollment(request):
"""
if 'enrollment_action' in request.POST:
try:
enrollment_output = change_enrollment(request)
enrollment_response = change_enrollment(request)
# There isn't really a way to display the results to the user, so we just log it
# We expect the enrollment to be a success, and will show up on the dashboard anyway
log.info("Attempted to automatically enroll after login. Results: {0}".format(enrollment_output))
log.info(
"Attempted to automatically enroll after login. Response code: {0}; response body: {1}".format(
enrollment_response.status_code,
enrollment_response.content
)
)
except Exception, e:
log.exception("Exception automatically enrolling after login: {0}".format(str(e)))
@login_required
def change_enrollment_view(request):
"""Delegate to change_enrollment to actually do the work."""
return HttpResponse(json.dumps(change_enrollment(request)))
def change_enrollment(request):
"""
Modify the enrollment status for the logged-in user.
The request parameter must be a POST request (other methods return 405)
that specifies course_id and enrollment_action parameters. If course_id or
enrollment_action is not specified, if course_id is not valid, if
enrollment_action is something other than "enroll" or "unenroll", if
enrollment_action is "enroll" and enrollment is closed for the course, or
if enrollment_action is "unenroll" and the user is not enrolled in the
course, a 400 error will be returned. If the user is not logged in, 403
will be returned; it is important that only this case return 403 so the
front end can redirect the user to a registration or login page when this
happens. This function should only be called from an AJAX request or
as a post-login/registration helper, so the error messages in the responses
should never actually be user-visible.
"""
if request.method != "POST":
raise Http404
return HttpResponseNotAllowed(["POST"])
user = request.user
if not user.is_authenticated():
raise Http404
return HttpResponseForbidden()
action = request.POST.get("enrollment_action", "")
course_id = request.POST.get("course_id", None)
action = request.POST.get("enrollment_action")
course_id = request.POST.get("course_id")
if course_id is None:
return HttpResponse(json.dumps({'success': False,
'error': 'There was an error receiving the course id.'}))
return HttpResponseBadRequest("Course id not specified")
if action == "enroll":
# Make sure the course exists
@@ -313,12 +356,10 @@ def change_enrollment(request):
except ItemNotFoundError:
log.warning("User {0} tried to enroll in non-existent course {1}"
.format(user.username, course_id))
return {'success': False, 'error': 'The course requested does not exist.'}
return HttpResponseBadRequest("Course id is invalid")
if not has_access(user, course, 'enroll'):
return {'success': False,
'error': 'enrollment in {} not allowed at this time'
.format(course.display_name_with_default)}
return HttpResponseBadRequest("Enrollment is closed")
org, course_num, run = course_id.split("/")
statsd.increment("common.student.enrollment",
@@ -332,7 +373,7 @@ def change_enrollment(request):
# If we've already created this enrollment in a separate transaction,
# then just continue
pass
return {'success': True}
return HttpResponse()
elif action == "unenroll":
try:
@@ -345,21 +386,17 @@ def change_enrollment(request):
"course:{0}".format(course_num),
"run:{0}".format(run)])
return {'success': True}
return HttpResponse()
except CourseEnrollment.DoesNotExist:
return {'success': False, 'error': 'You are not enrolled for this course.'}
return HttpResponseBadRequest("You are not enrolled in this course")
else:
return {'success': False, 'error': 'Invalid enrollment_action.'}
return {'success': False, 'error': 'We weren\'t able to unenroll you. Please try again.'}
return HttpResponseBadRequest("Enrollment action is invalid")
@ensure_csrf_cookie
def accounts_login(request, error=""):
return render_to_response('accounts_login.html', {'error': error})
return render_to_response('login.html', {'error': error})
# Need different levels of logging
@@ -403,8 +440,29 @@ def login_user(request, error=""):
try_change_enrollment(request)
statsd.increment("common.student.successful_login")
response = HttpResponse(json.dumps({'success': True}))
return HttpResponse(json.dumps({'success': True}))
# set the login cookie for the edx marketing site
# we want this cookie to be accessed via javascript
# so httponly is set to None
if request.session.get_expire_at_browser_close():
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path='/',
secure=None,
httponly=None)
return response
log.warning(u"Login failed - Account not active for user {0}, resending activation".format(username))
@@ -418,9 +476,18 @@ def login_user(request, error=""):
@ensure_csrf_cookie
def logout_user(request):
''' HTTP request to log out the user. Redirects to marketing page'''
'''
HTTP request to log out the user. Redirects to marketing page.
Deletes both the CSRF and sessionid cookies so the marketing
site can determine the logged in state of the user
'''
logout(request)
return redirect('/')
response = redirect('/')
response.delete_cookie(settings.EDXMKTG_COOKIE_NAME,
path='/',
domain=settings.SESSION_COOKIE_DOMAIN)
return response
@login_required
@@ -615,7 +682,31 @@ def create_account(request, post_override=None):
statsd.increment("common.student.account_created")
js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json")
HttpResponse(json.dumps(js), mimetype="application/json")
response = HttpResponse(json.dumps({'success': True}))
# set the login cookie for the edx marketing site
# we want this cookie to be accessed via javascript
# so httponly is set to None
if request.session.get_expire_at_browser_close():
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
response.set_cookie(settings.EDXMKTG_COOKIE_NAME,
'true', max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path='/',
secure=None,
httponly=None)
return response
def exam_registration_info(user, course):
@@ -701,7 +792,6 @@ def create_exam_registration(request, post_override=None):
for fieldname in TestCenterUser.user_provided_fields():
if fieldname in post_vars:
demographic_data[fieldname] = (post_vars[fieldname]).strip()
try:
testcenter_user = TestCenterUser.objects.get(user=user)
needs_updating = testcenter_user.needs_update(demographic_data)

View File

@@ -1,50 +1,102 @@
"""
Browser set up for acceptance tests.
"""
#pylint: disable=E1101
#pylint: disable=W0613
#pylint: disable=W0611
from lettuce import before, after, world
from splinter.browser import Browser
from logging import getLogger
from django.core.management import call_command
from django.conf import settings
from selenium.common.exceptions import WebDriverException
# Let the LMS and CMS do their one-time setup
# For example, setting up mongo caches
from lms import one_time_startup
from cms import one_time_startup
logger = getLogger(__name__)
logger.info("Loading the lettuce acceptance testing terrain file...")
# There is an import issue when using django-staticfiles with lettuce
# Lettuce assumes that we are using django.contrib.staticfiles,
# but the rest of the app assumes we are using django-staticfiles
# (in particular, django-pipeline and our mako implementation)
# To resolve this, we check whether staticfiles is installed,
# then redirect imports for django.contrib.staticfiles
# to use staticfiles.
try:
import staticfiles
except ImportError:
pass
else:
import sys
sys.modules['django.contrib.staticfiles'] = staticfiles
LOGGER = getLogger(__name__)
LOGGER.info("Loading the lettuce acceptance testing terrain file...")
MAX_VALID_BROWSER_ATTEMPTS = 20
@before.harvest
def initial_setup(server):
'''
Launch the browser once before executing the tests
'''
"""
Launch the browser once before executing the tests.
"""
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
world.browser = Browser(browser_driver)
# There is an issue with ChromeDriver2 r195627 on Ubuntu
# in which we sometimes get an invalid browser session.
# This is a work-around to ensure that we get a valid session.
success = False
num_attempts = 0
while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
# Get a browser session
world.browser = Browser(browser_driver)
# Try to visit the main page
# If the browser session is invalid, this will
# raise a WebDriverException
try:
world.visit('/')
except WebDriverException:
world.browser.quit()
num_attempts += 1
else:
success = True
# If we were unable to get a valid session within the limit of attempts,
# then we cannot run the tests.
if not success:
raise IOError("Could not acquire valid ChromeDriver browser session.")
@before.each_scenario
def reset_data(scenario):
'''
"""
Clean out the django test database defined in the
envs/acceptance.py file: mitx_all/db/test_mitx.db
'''
logger.debug("Flushing the test database...")
"""
LOGGER.debug("Flushing the test database...")
call_command('flush', interactive=False)
@after.each_scenario
def screenshot_on_error(scenario):
'''
Save a screenshot to help with debugging
'''
"""
Save a screenshot to help with debugging.
"""
if scenario.failed:
world.browser.driver.save_screenshot('/tmp/last_failed_scenario.png')
@after.all
def teardown_browser(total):
'''
Quit the browser after executing the tests
'''
"""
Quit the browser after executing the tests.
"""
world.browser.quit()
pass

View File

@@ -38,9 +38,11 @@ def create_user(uname):
@world.absorb
def log_in(username, password):
'''
Log the user in programatically
'''
"""
Log the user in programatically.
This will delete any existing cookies to ensure that the user
logs in to the correct session.
"""
# Authenticate the user
user = authenticate(username=username, password=password)
@@ -60,15 +62,8 @@ def log_in(username, password):
# Retrieve the sessionid and add it to the browser's cookies
cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
try:
world.browser.cookies.add(cookie_dict)
# WebDriver has an issue where we cannot set cookies
# before we make a GET request, so if we get an error,
# we load the '/' page and try again
except:
world.browser.visit(django_url('/'))
world.browser.cookies.add(cookie_dict)
world.browser.cookies.delete()
world.browser.cookies.add(cookie_dict)
@world.absorb

View File

@@ -122,6 +122,13 @@ def should_see_a_link_called(step, text):
assert len(world.browser.find_link_by_text(text)) > 0
@step(r'should see (?:the|a) link with the id "([^"]*)" called "([^"]*)"$')
def should_have_link_with_id_and_text(step, link_id, text):
link = world.browser.find_by_id(link_id)
assert len(link) > 0
assert_equals(link.text, text)
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
def should_see_in_the_page(step, text):
assert_in(text, world.css_text('body'))
@@ -144,3 +151,8 @@ def i_am_an_edx_user(step):
@step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname):
world.create_user(uname)
@step(u'All dialogs should be closed$')
def dialogs_are_closed(step):
assert world.dialogs_closed()

View File

@@ -1,7 +1,7 @@
#pylint: disable=C0111
#pylint: disable=W0621
from lettuce import world, step
from lettuce import world
import time
from urllib import quote_plus
from selenium.common.exceptions import WebDriverException
@@ -53,12 +53,9 @@ def css_find(css):
@world.absorb
def css_click(css_selector):
'''
First try to use the regular click method,
but if clicking in the middle of an element
doesn't work it might be that it thinks some other
element is on top of it there so click in the upper left
'''
"""
Perform a click on a CSS selector, retrying if it initially fails
"""
try:
world.browser.find_by_css(css_selector).click()
@@ -107,6 +104,17 @@ def css_visible(css_selector):
return world.browser.find_by_css(css_selector).visible
@world.absorb
def dialogs_closed():
def are_dialogs_closed(driver):
'''
Return True when no modal dialogs are visible
'''
return not css_visible('.modal')
wait_for(are_dialogs_closed)
return not css_visible('.modal')
@world.absorb
def save_the_html(path='/tmp'):
u = world.browser.url
@@ -114,4 +122,4 @@ def save_the_html(path='/tmp'):
filename = '%s.html' % quote_plus(u)
f = open('%s/%s' % (path, filename), 'w')
f.write(html)
f.close
f.close()

View File

@@ -16,7 +16,7 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from urllib import urlencode
import zendesk
import capa.calc
import calc
import track.views
@@ -27,7 +27,7 @@ def calculate(request):
''' Calculator in footer of every page. '''
equation = request.GET['equation']
try:
result = capa.calc.evaluator({}, {}, equation)
result = calc.evaluator({}, {}, equation)
except:
event = {'error': map(str, sys.exc_info()),
'equation': equation}

View File

@@ -1 +0,0 @@
*/jasmine_test_runner.html

12
common/lib/calc/setup.py Normal file
View File

@@ -0,0 +1,12 @@
from setuptools import setup
setup(
name="calc",
version="0.1",
py_modules=["calc"],
install_requires=[
"pyparsing==1.5.6",
"numpy",
"scipy"
],
)

View File

@@ -13,33 +13,19 @@ Main module which shows problems (of "capa" type).
This is used by capa_module.
'''
from __future__ import division
from datetime import datetime
import logging
import math
import numpy
import os
import random
import os.path
import re
import scipy
import struct
import sys
from lxml import etree
from xml.sax.saxutils import unescape
from copy import deepcopy
import chem
import chem.miller
import chem.chemcalc
import chem.chemtools
import verifiers
import verifiers.draganddrop
import calc
from .correctmap import CorrectMap
import eia
import inputtypes
import customrender
from .util import contextualize_text, convert_files_to_filenames
@@ -47,6 +33,7 @@ import xqueue_interface
# to be replaced with auto-registering
import responsetypes
import safe_exec
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
@@ -63,17 +50,6 @@ html_transforms = {'problem': {'tag': 'div'},
"math": {'tag': 'span'},
}
global_context = {'random': random,
'numpy': numpy,
'math': math,
'scipy': scipy,
'calc': calc,
'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
@@ -96,7 +72,7 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- seed (int): random number generator seed (int)
- seed (int): random number generator seed (int)
- state (dict): containing the following keys:
- 'seed' - (int) random number generator seed
- 'student_answers' - (dict) maps input id to the stored answer for that input
@@ -115,23 +91,20 @@ class LoncapaProblem(object):
if self.system is None:
raise Exception()
state = state if state else {}
state = state or {}
# Set seed according to the following priority:
# 1. Contained in problem's state
# 2. Passed into capa_problem via constructor
# 3. Assign from the OS's random number generator
self.seed = state.get('seed', seed)
if self.seed is None:
self.seed = struct.unpack('i', os.urandom(4))[0]
assert self.seed is not None, "Seed must be provided for LoncapaProblem."
self.student_answers = state.get('student_answers', {})
if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map'])
self.done = state.get('done', False)
self.input_state = state.get('input_state', {})
# Convert startouttext and endouttext to proper <text></text>
problem_text = re.sub("startouttext\s*/", "text", problem_text)
problem_text = re.sub("endouttext\s*/", "/text", problem_text)
@@ -144,7 +117,7 @@ class LoncapaProblem(object):
self._process_includes()
# construct script processor context (eg for customresponse problems)
self.context = self._extract_context(self.tree, seed=self.seed)
self.context = self._extract_context(self.tree)
# Pre-parse the XML tree: modifies it to add ID's and perform some in-place
# transformations. This also creates the dict (self.responders) of Response
@@ -440,18 +413,23 @@ class LoncapaProblem(object):
path = []
for dir in raw_path:
if not dir:
continue
# path is an absolute path or a path relative to the data dir
dir = os.path.join(self.system.filestore.root_path, dir)
# Check that we are within the filestore tree.
reldir = os.path.relpath(dir, self.system.filestore.root_path)
if ".." in reldir:
log.warning("Ignoring Python directory outside of course: %r" % dir)
continue
abs_dir = os.path.normpath(dir)
path.append(abs_dir)
return path
def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
def _extract_context(self, tree):
'''
Extract content of <script>...</script> from the problem.xml file, and exec it in the
context of this problem. Provides ability to randomize problems, and also set
@@ -459,55 +437,47 @@ class LoncapaProblem(object):
Problem XML goes to Python execution context. Runs everything in script tags.
'''
random.seed(self.seed)
# save global context in here also
context = {'global_context': global_context}
context = {}
context['seed'] = self.seed
all_code = ''
# initialize context to have stuff in global_context
context.update(global_context)
python_path = []
# put globals there also
context['__builtins__'] = globals()['__builtins__']
# pass instance of LoncapaProblem in
context['the_lcp'] = self
context['script_code'] = ''
self._execute_scripts(tree.findall('.//script'), context)
return context
def _execute_scripts(self, scripts, context):
'''
Executes scripts in the given context.
'''
original_path = sys.path
for script in scripts:
sys.path = original_path + self._extract_system_path(script)
for script in tree.findall('.//script'):
stype = script.get('type')
if stype:
if 'javascript' in stype:
continue # skip javascript
if 'perl' in stype:
continue # skip perl
# TODO: evaluate only python
code = script.text
for d in self._extract_system_path(script):
if d not in python_path and os.path.exists(d):
python_path.append(d)
XMLESC = {"&apos;": "'", "&quot;": '"'}
code = unescape(code, XMLESC)
# store code source in context
context['script_code'] += code
code = unescape(script.text, XMLESC)
all_code += code
if all_code:
try:
# use "context" for global context; thus defs in code are global within code
exec code in context, context
safe_exec.safe_exec(
all_code,
context,
random_seed=self.seed,
python_path=python_path,
cache=self.system.cache,
)
except Exception as err:
log.exception("Error while execing script code: " + code)
log.exception("Error while execing script code: " + all_code)
msg = "Error while executing script code: %s" % str(err).replace('<', '&lt;')
raise responsetypes.LoncapaProblemError(msg)
finally:
sys.path = original_path
# store code source in context
context['script_code'] = all_code
return context

View File

@@ -46,7 +46,7 @@ import sys
import pyparsing
from .registry import TagRegistry
from capa.chem import chemcalc
from chem import chemcalc
import xqueue_interface
from datetime import datetime

View File

@@ -23,6 +23,7 @@ import random
import re
import requests
import subprocess
import textwrap
import traceback
import xml.sax.saxutils as saxutils
@@ -30,17 +31,23 @@ from collections import namedtuple
from shapely.geometry import Point, MultiPoint
# specific library imports
from .calc import evaluator, UndefinedVariable
from .correctmap import CorrectMap
from calc import evaluator, UndefinedVariable
from . import correctmap
from datetime import datetime
from .util import *
from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import capa.xqueue_interface as xqueue_interface
import safe_exec
log = logging.getLogger(__name__)
CorrectMap = correctmap.CorrectMap
CORRECTMAP_PY = None
#-----------------------------------------------------------------------------
# Exceptions
@@ -252,20 +259,41 @@ class LoncapaResponse(object):
# We may extend this in the future to add another argument which provides a
# callback procedure to a social hint generation system.
if not hintfn in self.context:
msg = 'missing specified hint function %s in script context' % hintfn
msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg)
global CORRECTMAP_PY
if CORRECTMAP_PY is None:
# We need the CorrectMap code for hint functions. No, this is not great.
CORRECTMAP_PY = inspect.getsource(correctmap)
code = (
CORRECTMAP_PY + "\n" +
self.context['script_code'] + "\n" +
textwrap.dedent("""
new_cmap = CorrectMap()
new_cmap.set_dict(new_cmap_dict)
old_cmap = CorrectMap()
old_cmap.set_dict(old_cmap_dict)
{hintfn}(answer_ids, student_answers, new_cmap, old_cmap)
new_cmap_dict.update(new_cmap.get_dict())
old_cmap_dict.update(old_cmap.get_dict())
""").format(hintfn=hintfn)
)
globals_dict = {
'answer_ids': self.answer_ids,
'student_answers': student_answers,
'new_cmap_dict': new_cmap.get_dict(),
'old_cmap_dict': old_cmap.get_dict(),
}
try:
self.context[hintfn](
self.answer_ids, student_answers, new_cmap, old_cmap)
safe_exec.safe_exec(code, globals_dict)
except Exception as err:
msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>')
raise ResponseError(msg)
new_cmap.set_dict(globals_dict['new_cmap_dict'])
return
# hint specified by conditions and text dependent on conditions (a-la Loncapa design)
@@ -475,6 +503,10 @@ class JavascriptResponse(LoncapaResponse):
return tmp_env
def call_node(self, args):
# Node.js code is un-sandboxed. If the XModuleSystem says we aren't
# allowed to run unsafe code, then stop now.
if not self.system.can_execute_unsafe_code():
raise LoncapaProblemError("Execution of unsafe Javascript code is not allowed.")
subprocess_args = ["node"]
subprocess_args.extend(args)
@@ -488,7 +520,7 @@ class JavascriptResponse(LoncapaResponse):
output = self.call_node([generator_file,
self.generator,
json.dumps(self.generator_dependencies),
json.dumps(str(self.context['the_lcp'].seed)),
json.dumps(str(self.context['seed'])),
json.dumps(self.params)]).strip()
return json.loads(output)
@@ -660,15 +692,6 @@ class ChoiceResponse(LoncapaResponse):
class MultipleChoiceResponse(LoncapaResponse):
# TODO: handle direction and randomize
snippets = [{'snippet': '''<multiplechoiceresponse direction="vertical" randomize="yes">
<choicegroup type="MultipleChoice">
<choice location="random" correct="false"><span>`a+b`<br/></span></choice>
<choice location="random" correct="true"><span><math>a+b^2</math><br/></span></choice>
<choice location="random" correct="false"><math>a+b+c</math></choice>
<choice location="bottom" correct="false"><math>a+b+d</math></choice>
</choicegroup>
</multiplechoiceresponse>
'''}]
response_tag = 'multiplechoiceresponse'
max_inputfields = 1
@@ -754,14 +777,6 @@ class OptionResponse(LoncapaResponse):
'''
TODO: handle direction and randomize
'''
snippets = [{'snippet': """<optionresponse direction="vertical" randomize="yes">
<optioninput options="('Up','Down')" correct="Up">
<text>The location of the sky</text>
</optioninput>
<optioninput options="('Up','Down')" correct="Down">
<text>The location of the earth</text>
</optioninput>
</optionresponse>"""}]
response_tag = 'optionresponse'
hint_tag = 'optionhint'
@@ -905,39 +920,6 @@ class CustomResponse(LoncapaResponse):
Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script>
'''
snippets = [{'snippet': r"""<customresponse>
<text>
<br/>
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
In the space provided below write an algebraic expression for \(I(t)\).
<br/>
<textline size="5" correct_answer="IS*u(t-t0)" />
</text>
<answer type="loncapa/python">
correct=['correct']
try:
r = str(submission[0])
except ValueError:
correct[0] ='incorrect'
r = '0'
if not(r=="IS*u(t-t0)"):
correct[0] ='incorrect'
</answer>
</customresponse>"""},
{'snippet': """<script type="loncapa/python"><![CDATA[
def sympy_check2():
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','&lt;'))
#messages[0] = str(answers)
correct[0] = 'correct'
]]>
</script>
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
<textline size="40" dojs="math" />
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
</customresponse>"""}]
response_tag = 'customresponse'
@@ -972,14 +954,29 @@ def sympy_check2():
cfn = xml.get('cfn')
if cfn:
log.debug("cfn = %s" % cfn)
if cfn in self.context:
self.code = self.context[cfn]
else:
msg = "%s: can't find cfn %s in context" % (
unicode(self), cfn)
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline',
'<unavailable>')
raise LoncapaProblemError(msg)
# This is a bit twisty. We used to grab the cfn function from
# the context, but now that we sandbox Python execution, we
# can't get functions from previous executions. So we make an
# actual function that will re-execute the original script,
# and invoke the function with the data needed.
def make_check_function(script_code, cfn):
def check_function(expect, ans, **kwargs):
extra_args = "".join(", {0}={0}".format(k) for k in kwargs)
code = (
script_code + "\n" +
"cfn_return = %s(expect, ans%s)\n" % (cfn, extra_args)
)
globals_dict = {
'expect': expect,
'ans': ans,
}
globals_dict.update(kwargs)
safe_exec.safe_exec(code, globals_dict, cache=self.system.cache)
return globals_dict['cfn_return']
return check_function
self.code = make_check_function(self.context['script_code'], cfn)
if not self.code:
if answer is None:
@@ -1036,9 +1033,6 @@ def sympy_check2():
# put these in the context of the check function evaluator
# note that this doesn't help the "cfn" version - only the exec version
self.context.update({
# our subtree
'xml': self.xml,
# my ID
'response_id': self.myid,
@@ -1075,65 +1069,63 @@ def sympy_check2():
# pass self.system.debug to cfn
self.context['debug'] = self.system.DEBUG
# Run the check function
self.execute_check_function(idset, submission)
# build map giving "correct"ness of the answer(s)
correct = self.context['correct']
messages = self.context['messages']
overall_message = self.clean_message_html(self.context['overall_message'])
correct_map = CorrectMap()
correct_map.set_overall_message(overall_message)
for k in range(len(idset)):
npoints = self.maxpoints[idset[k]] if correct[k] == 'correct' else 0
correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints)
return correct_map
def execute_check_function(self, idset, submission):
# exec the check function
if isinstance(self.code, basestring):
try:
exec self.code in self.context['global_context'], self.context
correct = self.context['correct']
messages = self.context['messages']
overall_message = self.context['overall_message']
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache)
except Exception as err:
self._handle_exec_exception(err)
else:
# self.code is not a string; assume its a function
# self.code is not a string; it's a function we created earlier.
# this is an interface to the Tutor2 check functions
fn = self.code
ret = None
answer_given = submission[0] if (len(idset) == 1) else submission
kwnames = self.xml.get("cfn_extra_args", "").split()
kwargs = {n:self.context.get(n) for n in kwnames}
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
# with various Tutor2 check functions
args = [self.expect, answer_given,
student_answers, self.answer_ids[0]]
argspec = inspect.getargspec(fn)
nargs = len(argspec.args) - len(argspec.defaults or [])
kwargs = {}
for argname in argspec.args[nargs:]:
kwargs[argname] = self.context[
argname] if argname in self.context else None
log.debug('[customresponse] answer_given=%s' % answer_given)
log.debug('nargs=%d, args=%s, kwargs=%s' % (
nargs, args, kwargs))
ret = fn(*args[:nargs], **kwargs)
ret = fn(self.expect, answer_given, **kwargs)
except Exception as err:
self._handle_exec_exception(err)
if type(ret) == dict:
log.debug(
"[courseware.capa.responsetypes.customresponse.get_score] ret = %s",
ret
)
if isinstance(ret, dict):
# One kind of dictionary the check function can return has the
# form {'ok': BOOLEAN, 'msg': STRING}
# If there are multiple inputs, they all get marked
# to the same correct/incorrect value
if 'ok' in ret:
correct = ['correct'] * len(idset) if ret[
'ok'] else ['incorrect'] * len(idset)
correct = ['correct' if ret['ok'] else 'incorrect'] * len(idset)
msg = ret.get('msg', None)
msg = self.clean_message_html(msg)
# If there is only one input, apply the message to that input
# Otherwise, apply the message to the whole problem
if len(idset) > 1:
overall_message = msg
self.context['overall_message'] = msg
else:
messages[0] = msg
self.context['messages'][0] = msg
# Another kind of dictionary the check function can return has
# the form:
@@ -1155,6 +1147,8 @@ def sympy_check2():
msg = (self.clean_message_html(input_dict['msg'])
if 'msg' in input_dict else None)
messages.append(msg)
self.context['messages'] = messages
self.context['overall_message'] = overall_message
# Otherwise, we do not recognize the dictionary
# Raise an exception
@@ -1163,25 +1157,10 @@ def sympy_check2():
raise ResponseError(
"CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value,
# indicating whether all inputs should be marked
# correct or incorrect
else:
n = len(idset)
correct = ['correct'] * n if ret else ['incorrect'] * n
correct = ['correct' if ret else 'incorrect'] * len(idset)
# build map giving "correct"ness of the answer(s)
correct_map = CorrectMap()
overall_message = self.clean_message_html(overall_message)
correct_map.set_overall_message(overall_message)
for k in range(len(idset)):
npoints = (self.maxpoints[idset[k]]
if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints)
return correct_map
self.context['correct'] = correct
def clean_message_html(self, msg):
@@ -1253,24 +1232,38 @@ class SymbolicResponse(CustomResponse):
"""
Symbolic math response checking, using symmath library.
"""
snippets = [{'snippet': r'''<problem>
<text>Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
and give the resulting \(2\times 2\) matrix: <br/>
<symbolicresponse answer="">
<textline size="40" math="1" />
</symbolicresponse>
<br/>
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>.
</text>
</problem>'''}]
response_tag = 'symbolicresponse'
max_inputfields = 1
def setup_response(self):
# Symbolic response always uses symmath_check()
# If the XML did not specify this, then set it now
# Otherwise, we get an error from the superclass
self.xml.set('cfn', 'symmath_check')
code = "from symmath import *"
exec code in self.context, self.context
CustomResponse.setup_response(self)
# Let CustomResponse do its setup
super(SymbolicResponse, self).setup_response()
def execute_check_function(self, idset, submission):
from symmath import symmath_check
try:
# Since we have limited max_inputfields to 1,
# we can assume that there is only one submission
answer_given = submission[0]
ret = symmath_check(
self.expect, answer_given,
dynamath=self.context.get('dynamath'),
options=self.context.get('options'),
debug=self.context.get('debug'),
)
except Exception as err:
log.error("oops in symbolicresponse (cfn) error %s" % err)
log.error(traceback.format_exc())
raise Exception("oops in symbolicresponse (cfn) error %s" % err)
self.context['messages'][0] = self.clean_message_html(ret['msg'])
self.context['correct'] = ['correct' if ret['ok'] else 'incorrect'] * len(idset)
#-----------------------------------------------------------------------------
@@ -1325,10 +1318,8 @@ class CodeResponse(LoncapaResponse):
# Check if XML uses the ExternalResponse format or the generic
# CodeResponse format
codeparam = self.xml.find('codeparam')
if codeparam is None:
self._parse_externalresponse_xml()
else:
self._parse_coderesponse_xml(codeparam)
assert codeparam is not None, "Unsupported old format! <coderesponse> without <codeparam>"
self._parse_coderesponse_xml(codeparam)
def _parse_coderesponse_xml(self, codeparam):
'''
@@ -1348,62 +1339,6 @@ class CodeResponse(LoncapaResponse):
self.answer = find_with_default(codeparam, 'answer_display',
'No answer provided.')
def _parse_externalresponse_xml(self):
'''
VS[compat]: Suppport for old ExternalResponse XML format. When successful, sets:
self.initial_display
self.answer (an answer to display to the student in the LMS)
self.payload
'''
answer = self.xml.find('answer')
if answer is not None:
answer_src = answer.get('src')
if answer_src is not None:
code = self.system.filesystem.open('src/' + answer_src).read()
else:
code = answer.text
else: # no <answer> stanza; get code from <script>
code = self.context['script_code']
if not code:
msg = '%s: Missing answer script code for coderesponse' % unicode(
self)
msg += "\nSee XML source line %s" % getattr(
self.xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg)
tests = self.xml.get('tests')
# Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is:
# (1) Internal edX code, i.e. NOT student submissions, and
# (2) The code should only define the strings 'initial_display', 'answer',
# 'preamble', 'test_program'
# following the ExternalResponse XML format
penv = {}
penv['__builtins__'] = globals()['__builtins__']
try:
exec(code, penv, penv)
except Exception as err:
log.error(
'Error in CodeResponse %s: Error in problem reference code' % err)
raise Exception(err)
try:
self.answer = penv['answer']
self.initial_display = penv['initial_display']
except Exception as err:
log.error("Error in CodeResponse %s: Problem reference code does not define"
" 'answer' and/or 'initial_display' in <answer>...</answer>" % err)
raise Exception(err)
# Finally, make the ExternalResponse input XML format conform to the generic
# exteral grader interface
# The XML tagging of grader_payload is pyxserver-specific
grader_payload = '<pyxserver>'
grader_payload += '<tests>' + tests + '</tests>\n'
grader_payload += '<processor>' + code + '</processor>'
grader_payload += '</pyxserver>'
self.payload = {'grader_payload': grader_payload}
def get_score(self, student_answers):
try:
# Note that submission can be a file
@@ -1583,44 +1518,6 @@ class ExternalResponse(LoncapaResponse):
Typically used by coding problems.
'''
snippets = [{'snippet': '''<externalresponse tests="repeat:10,generate">
<textbox rows="10" cols="70" mode="python"/>
<answer><![CDATA[
initial_display = """
def inc(x):
"""
answer = """
def inc(n):
return n+1
"""
preamble = """
import sympy
"""
test_program = """
import random
def testInc(n = None):
if n is None:
n = random.randint(2, 20)
print 'Test is: inc(%d)'%n
return str(inc(n))
def main():
f = os.fdopen(3,'w')
test = int(sys.argv[1])
rndlist = map(int,os.getenv('rndlist').split(','))
random.seed(rndlist[0])
if test == 1: f.write(testInc(0))
elif test == 2: f.write(testInc(1))
else: f.write(testInc())
f.close()
main()
"""
]]>
</answer>
</externalresponse>'''}]
response_tag = 'externalresponse'
allowed_inputfields = ['textline', 'textbox']
@@ -1766,23 +1663,6 @@ class FormulaResponse(LoncapaResponse):
'''
Checking of symbolic math response using numerical sampling.
'''
snippets = [{'snippet': '''<problem>
<script type="loncapa/python">
I = "m*c^2"
</script>
<text>
<br/>
Give an equation for the relativistic energy of an object with mass m.
</text>
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="$I">
<responseparam description="Numerical Tolerance" type="tolerance"
default="0.00001" name="tol" />
<textline size="40" math="1" />
</formularesponse>
</problem>'''}]
response_tag = 'formularesponse'
hint_tag = 'formulahint'
@@ -1927,21 +1807,18 @@ class SchematicResponse(LoncapaResponse):
self.code = answer.text
def get_score(self, student_answers):
from capa_problem import global_context
submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)]
#from capa_problem import global_context
submission = [
json.loads(student_answers[k]) for k in sorted(self.answer_ids)
]
self.context.update({'submission': submission})
try:
exec self.code in global_context, self.context
safe_exec.safe_exec(self.code, self.context, cache=self.system.cache)
except Exception as err:
_, _, traceback_obj = sys.exc_info()
raise ResponseError, ResponseError(err.message), traceback_obj
msg = 'Error %s in evaluating SchematicResponse' % err
raise ResponseError(msg)
cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct'])))
cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct'])))
return cmap
def get_answers(self):
@@ -1977,19 +1854,6 @@ class ImageResponse(LoncapaResponse):
Returns:
True, if click is inside any region or rectangle. Otherwise False.
"""
snippets = [{'snippet': '''<imageresponse>
<imageinput src="image1.jpg" width="200" height="100"
rectangle="(10,10)-(20,30)" />
<imageinput src="image2.jpg" width="210" height="130"
rectangle="(12,12)-(40,60)" />
<imageinput src="image3.jpg" width="210" height="130"
rectangle="(10,10)-(20,30);(12,12)-(40,60)" />
<imageinput src="image4.jpg" width="811" height="610"
rectangle="(10,10)-(20,30);(12,12)-(40,60)"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
<imageinput src="image5.jpg" width="200" height="200"
regions="[[[10,10], [20,30], [40, 10]], [[100,100], [120,130], [110,150]]]"/>
</imageresponse>'''}]
response_tag = 'imageresponse'
allowed_inputfields = ['imageinput']

View File

@@ -0,0 +1,51 @@
Configuring Capa sandboxed execution
====================================
Capa problems can contain code authored by the course author. We need to
execute that code in a sandbox. We use CodeJail as the sandboxing facility,
but it needs to be configured specifically for Capa's use.
As a developer, you don't have to do anything to configure sandboxing if you
don't want to, and everything will operate properly, you just won't have
protection on that code.
If you want to configure sandboxing, you're going to use the `README from
CodeJail`__, with a few customized tweaks.
__ https://github.com/edx/codejail/blob/master/README.rst
1. At the instruction to install packages into the sandboxed code, you'll
need to install both `pre-sandbox-requirements.txt` and
`sandbox-requirements.txt`::
$ sudo pip install -r pre-sandbox-requirements.txt
$ sudo pip install -r sandbox-requirements.txt
2. At the instruction to create the AppArmor profile, you'll need a line in
the profile for the sandbox packages. <EDXPLATFORM> is the full path to
your edx_platform repo::
<EDXPLATFORM>/common/lib/sandbox-packages/** r,
3. You can configure resource limits in settings.py. A CODE_JAIL setting is
available, a dictionary. The "limits" key lets you adjust the limits for
CPU time, real time, and memory use. Setting any of them to zero disables
that limit::
# in settings.py...
CODE_JAIL = {
# Configurable limits.
'limits': {
# How many CPU seconds can jailed code use?
'CPU': 1,
# How many real-time seconds will a sandbox survive?
'REALTIME': 1,
# How much memory (in bytes) can a sandbox use?
'VMEM': 30000000,
},
}
That's it. Once you've finished the CodeJail configuration instructions,
your course-hosted Python code should be run securely.

View File

@@ -0,0 +1,3 @@
"""Capa's specialized use of codejail.safe_exec."""
from .safe_exec import safe_exec, update_hash

View File

@@ -0,0 +1,42 @@
"""A module proxy for delayed importing of modules.
From http://barnesc.blogspot.com/2006/06/automatic-python-imports-with-autoimp.html,
in the public domain.
"""
import sys
class LazyModule(object):
"""A lazy module proxy."""
def __init__(self, modname):
self.__dict__['__name__'] = modname
self._set_mod(None)
def _set_mod(self, mod):
if mod is not None:
self.__dict__ = mod.__dict__
self.__dict__['_lazymod_mod'] = mod
def _load_mod(self):
__import__(self.__name__)
self._set_mod(sys.modules[self.__name__])
def __getattr__(self, name):
if self.__dict__['_lazymod_mod'] is None:
self._load_mod()
mod = self.__dict__['_lazymod_mod']
if hasattr(mod, name):
return getattr(mod, name)
else:
try:
subname = '%s.%s' % (self.__name__, name)
__import__(subname)
submod = getattr(mod, name)
except ImportError:
raise AttributeError("'module' object has no attribute %r" % name)
self.__dict__[name] = LazyModule(subname, submod)
return self.__dict__[name]

View File

@@ -0,0 +1,130 @@
"""Capa's specialized use of codejail.safe_exec."""
from codejail.safe_exec import safe_exec as codejail_safe_exec
from codejail.safe_exec import json_safe, SafeExecException
from . import lazymod
from statsd import statsd
import hashlib
# Establish the Python environment for Capa.
# Capa assumes float-friendly division always.
# The name "random" is a properly-seeded stand-in for the random module.
CODE_PROLOG = """\
from __future__ import division
import random as random_module
import sys
random = random_module.Random(%r)
random.Random = random_module.Random
del random_module
sys.modules['random'] = random
"""
ASSUMED_IMPORTS=[
("numpy", "numpy"),
("math", "math"),
("scipy", "scipy"),
("calc", "calc"),
("eia", "eia"),
("chemcalc", "chem.chemcalc"),
("chemtools", "chem.chemtools"),
("miller", "chem.miller"),
("draganddrop", "verifiers.draganddrop"),
]
# We'll need the code from lazymod.py for use in safe_exec, so read it now.
lazymod_py_file = lazymod.__file__
if lazymod_py_file.endswith("c"):
lazymod_py_file = lazymod_py_file[:-1]
lazymod_py = open(lazymod_py_file).read()
LAZY_IMPORTS = [lazymod_py]
for name, modname in ASSUMED_IMPORTS:
LAZY_IMPORTS.append("{} = LazyModule('{}')\n".format(name, modname))
LAZY_IMPORTS = "".join(LAZY_IMPORTS)
def update_hash(hasher, obj):
"""
Update a `hashlib` hasher with a nested object.
To properly cache nested structures, we need to compute a hash from the
entire structure, canonicalizing at every level.
`hasher`'s `.update()` method is called a number of times, touching all of
`obj` in the process. Only primitive JSON-safe types are supported.
"""
hasher.update(str(type(obj)))
if isinstance(obj, (tuple, list)):
for e in obj:
update_hash(hasher, e)
elif isinstance(obj, dict):
for k in sorted(obj):
update_hash(hasher, k)
update_hash(hasher, obj[k])
else:
hasher.update(repr(obj))
@statsd.timed('capa.safe_exec.time')
def safe_exec(code, globals_dict, random_seed=None, python_path=None, cache=None):
"""
Execute python code safely.
`code` is the Python code to execute. It has access to the globals in `globals_dict`,
and any changes it makes to those globals are visible in `globals_dict` when this
function returns.
`random_seed` will be used to see the `random` module available to the code.
`python_path` is a list of directories to add to the Python path before execution.
`cache` is an object with .get(key) and .set(key, value) methods. It will be used
to cache the execution, taking into account the code, the values of the globals,
and the random seed.
"""
# Check the cache for a previous result.
if cache:
safe_globals = json_safe(globals_dict)
md5er = hashlib.md5()
md5er.update(repr(code))
update_hash(md5er, safe_globals)
key = "safe_exec.%r.%s" % (random_seed, md5er.hexdigest())
cached = cache.get(key)
if cached is not None:
# We have a cached result. The result is a pair: the exception
# message, if any, else None; and the resulting globals dictionary.
emsg, cleaned_results = cached
globals_dict.update(cleaned_results)
if emsg:
raise SafeExecException(emsg)
return
# Create the complete code we'll run.
code_prolog = CODE_PROLOG % random_seed
# Run the code! Results are side effects in globals_dict.
try:
codejail_safe_exec(
code_prolog + LAZY_IMPORTS + code, globals_dict,
python_path=python_path,
)
except SafeExecException as e:
emsg = e.message
else:
emsg = None
# Put the result back in the cache. This is complicated by the fact that
# the globals dict might not be entirely serializable.
if cache:
cleaned_results = json_safe(globals_dict)
cache.set(key, (emsg, cleaned_results))
# If an exception happened, raise it now.
if emsg:
raise e

View File

@@ -0,0 +1 @@
THE_CONST = 23

View File

@@ -0,0 +1,44 @@
"""Test lazymod.py"""
import sys
import unittest
from capa.safe_exec.lazymod import LazyModule
class ModuleIsolation(object):
"""
Manage changes to sys.modules so that we can roll back imported modules.
Create this object, it will snapshot the currently imported modules. When
you call `clean_up()`, it will delete any module imported since its creation.
"""
def __init__(self):
# Save all the names of all the imported modules.
self.mods = set(sys.modules)
def clean_up(self):
# Get a list of modules that didn't exist when we were created
new_mods = [m for m in sys.modules if m not in self.mods]
# and delete them all so another import will run code for real again.
for m in new_mods:
del sys.modules[m]
class TestLazyMod(unittest.TestCase):
def setUp(self):
# Each test will remove modules that it imported.
self.addCleanup(ModuleIsolation().clean_up)
def test_simple(self):
# Import some stdlib module that has not been imported before
self.assertNotIn("colorsys", sys.modules)
colorsys = LazyModule("colorsys")
hsv = colorsys.rgb_to_hsv(.3, .4, .2)
self.assertEqual(hsv[0], 0.25)
def test_dotted(self):
self.assertNotIn("email.utils", sys.modules)
email_utils = LazyModule("email.utils")
self.assertEqual(email_utils.quote('"hi"'), r'\"hi\"')

View File

@@ -0,0 +1,281 @@
"""Test safe_exec.py"""
import hashlib
import os.path
import random
import textwrap
import unittest
from capa.safe_exec import safe_exec, update_hash
from codejail.safe_exec import SafeExecException
class TestSafeExec(unittest.TestCase):
def test_set_values(self):
g = {}
safe_exec("a = 17", g)
self.assertEqual(g['a'], 17)
def test_division(self):
g = {}
# Future division: 1/2 is 0.5.
safe_exec("a = 1/2", g)
self.assertEqual(g['a'], 0.5)
def test_assumed_imports(self):
g = {}
# Math is always available.
safe_exec("a = int(math.pi)", g)
self.assertEqual(g['a'], 3)
def test_random_seeding(self):
g = {}
r = random.Random(17)
rnums = [r.randint(0, 999) for _ in xrange(100)]
# Without a seed, the results are unpredictable
safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g)
self.assertNotEqual(g['rnums'], rnums)
# With a seed, the results are predictable
safe_exec("rnums = [random.randint(0, 999) for _ in xrange(100)]", g, random_seed=17)
self.assertEqual(g['rnums'], rnums)
def test_random_is_still_importable(self):
g = {}
r = random.Random(17)
rnums = [r.randint(0, 999) for _ in xrange(100)]
# With a seed, the results are predictable even from the random module
safe_exec(
"import random\n"
"rnums = [random.randint(0, 999) for _ in xrange(100)]\n",
g, random_seed=17)
self.assertEqual(g['rnums'], rnums)
def test_python_lib(self):
pylib = os.path.dirname(__file__) + "/test_files/pylib"
g = {}
safe_exec(
"import constant; a = constant.THE_CONST",
g, python_path=[pylib]
)
def test_raising_exceptions(self):
g = {}
with self.assertRaises(SafeExecException) as cm:
safe_exec("1/0", g)
self.assertIn("ZeroDivisionError", cm.exception.message)
class DictCache(object):
"""A cache implementation over a simple dict, for testing."""
def __init__(self, d):
self.cache = d
def get(self, key):
# Actual cache implementations have limits on key length
assert len(key) <= 250
return self.cache.get(key)
def set(self, key, value):
# Actual cache implementations have limits on key length
assert len(key) <= 250
self.cache[key] = value
class TestSafeExecCaching(unittest.TestCase):
"""Test that caching works on safe_exec."""
def test_cache_miss_then_hit(self):
g = {}
cache = {}
# Cache miss
safe_exec("a = int(math.pi)", g, cache=DictCache(cache))
self.assertEqual(g['a'], 3)
# A result has been cached
self.assertEqual(cache.values()[0], (None, {'a': 3}))
# Fiddle with the cache, then try it again.
cache[cache.keys()[0]] = (None, {'a': 17})
g = {}
safe_exec("a = int(math.pi)", g, cache=DictCache(cache))
self.assertEqual(g['a'], 17)
def test_cache_large_code_chunk(self):
# Caching used to die on memcache with more than 250 bytes of code.
# Check that it doesn't any more.
code = "a = 0\n" + ("a += 1\n" * 12345)
g = {}
cache = {}
safe_exec(code, g, cache=DictCache(cache))
self.assertEqual(g['a'], 12345)
def test_cache_exceptions(self):
# Used to be that running code that raised an exception didn't cache
# the result. Check that now it does.
code = "1/0"
g = {}
cache = {}
with self.assertRaises(SafeExecException):
safe_exec(code, g, cache=DictCache(cache))
# The exception should be in the cache now.
self.assertEqual(len(cache), 1)
cache_exc_msg, cache_globals = cache.values()[0]
self.assertIn("ZeroDivisionError", cache_exc_msg)
# Change the value stored in the cache, the result should change.
cache[cache.keys()[0]] = ("Hey there!", {})
with self.assertRaises(SafeExecException):
safe_exec(code, g, cache=DictCache(cache))
self.assertEqual(len(cache), 1)
cache_exc_msg, cache_globals = cache.values()[0]
self.assertEqual("Hey there!", cache_exc_msg)
# Change it again, now no exception!
cache[cache.keys()[0]] = (None, {'a': 17})
safe_exec(code, g, cache=DictCache(cache))
self.assertEqual(g['a'], 17)
def test_unicode_submission(self):
# Check that using non-ASCII unicode does not raise an encoding error.
# Try several non-ASCII unicode characters
for code in [129, 500, 2**8 - 1, 2**16 - 1]:
code_with_unichr = unicode("# ") + unichr(code)
try:
safe_exec(code_with_unichr, {}, cache=DictCache({}))
except UnicodeEncodeError:
self.fail("Tried executing code with non-ASCII unicode: {0}".format(code))
class TestUpdateHash(unittest.TestCase):
"""Test the safe_exec.update_hash function to be sure it canonicalizes properly."""
def hash_obj(self, obj):
"""Return the md5 hash that `update_hash` makes us."""
md5er = hashlib.md5()
update_hash(md5er, obj)
return md5er.hexdigest()
def equal_but_different_dicts(self):
"""
Make two equal dicts with different key order.
Simple literals won't do it. Filling one and then shrinking it will
make them different.
"""
d1 = {k:1 for k in "abcdefghijklmnopqrstuvwxyz"}
d2 = dict(d1)
for i in xrange(10000):
d2[i] = 1
for i in xrange(10000):
del d2[i]
# Check that our dicts are equal, but with different key order.
self.assertEqual(d1, d2)
self.assertNotEqual(d1.keys(), d2.keys())
return d1, d2
def test_simple_cases(self):
h1 = self.hash_obj(1)
h10 = self.hash_obj(10)
hs1 = self.hash_obj("1")
self.assertNotEqual(h1, h10)
self.assertNotEqual(h1, hs1)
def test_list_ordering(self):
h1 = self.hash_obj({'a': [1,2,3]})
h2 = self.hash_obj({'a': [3,2,1]})
self.assertNotEqual(h1, h2)
def test_dict_ordering(self):
d1, d2 = self.equal_but_different_dicts()
h1 = self.hash_obj(d1)
h2 = self.hash_obj(d2)
self.assertEqual(h1, h2)
def test_deep_ordering(self):
d1, d2 = self.equal_but_different_dicts()
o1 = {'a':[1, 2, [d1], 3, 4]}
o2 = {'a':[1, 2, [d2], 3, 4]}
h1 = self.hash_obj(o1)
h2 = self.hash_obj(o2)
self.assertEqual(h1, h2)
class TestRealProblems(unittest.TestCase):
def test_802x(self):
code = textwrap.dedent("""\
import math
import random
import numpy
e=1.602e-19 #C
me=9.1e-31 #kg
mp=1.672e-27 #kg
eps0=8.854e-12 #SI units
mu0=4e-7*math.pi #SI units
Rd1=random.randrange(1,30,1)
Rd2=random.randrange(30,50,1)
Rd3=random.randrange(50,70,1)
Rd4=random.randrange(70,100,1)
Rd5=random.randrange(100,120,1)
Vd1=random.randrange(1,20,1)
Vd2=random.randrange(20,40,1)
Vd3=random.randrange(40,60,1)
#R=[0,10,30,50,70,100] #Ohm
#V=[0,12,24,36] # Volt
R=[0,Rd1,Rd2,Rd3,Rd4,Rd5] #Ohms
V=[0,Vd1,Vd2,Vd3] #Volts
#here the currents IL and IR are defined as in figure ps3_p3_fig2
a=numpy.array([ [ R[1]+R[4]+R[5],R[4] ],[R[4], R[2]+R[3]+R[4] ] ])
b=numpy.array([V[1]-V[2],-V[3]-V[2]])
x=numpy.linalg.solve(a,b)
IL='%.2e' % x[0]
IR='%.2e' % x[1]
ILR='%.2e' % (x[0]+x[1])
def sign(x):
return abs(x)/x
RW="Rightwards"
LW="Leftwards"
UW="Upwards"
DW="Downwards"
I1='%.2e' % abs(x[0])
I1d=LW if sign(x[0])==1 else RW
I1not=LW if I1d==RW else RW
I2='%.2e' % abs(x[1])
I2d=RW if sign(x[1])==1 else LW
I2not=LW if I2d==RW else RW
I3='%.2e' % abs(x[1])
I3d=DW if sign(x[1])==1 else UW
I3not=DW if I3d==UW else UW
I4='%.2e' % abs(x[0]+x[1])
I4d=UW if sign(x[1]+x[0])==1 else DW
I4not=DW if I4d==UW else UW
I5='%.2e' % abs(x[0])
I5d=RW if sign(x[0])==1 else LW
I5not=LW if I5d==RW else RW
VAP=-x[0]*R[1]-(x[0]+x[1])*R[4]
VPN=-V[2]
VGD=+V[1]-x[0]*R[1]+V[3]+x[1]*R[2]
aVAP='%.2e' % VAP
aVPN='%.2e' % VPN
aVGD='%.2e' % VGD
""")
g = {}
safe_exec(code, g)
self.assertIn("aVAP", g)

View File

@@ -1,7 +1,7 @@
import fs
import fs.osfs
import os
import os, os.path
from capa.capa_problem import LoncapaProblem
from mock import Mock, MagicMock
import xml.sax.saxutils as saxutils
@@ -22,16 +22,28 @@ def calledback_url(dispatch = 'score_update'):
xqueue_interface = MagicMock()
xqueue_interface.send_to_queue.return_value = (0, 'Success!')
test_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student'
)
def test_system():
"""
Construct a mock ModuleSystem instance.
"""
the_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student',
cache=None,
can_execute_unsafe_code=lambda: False,
)
return the_system
def new_loncapa_problem(xml, system=None):
"""Construct a `LoncapaProblem` suitable for unit tests."""
return LoncapaProblem(xml, id='1', seed=723, system=system or test_system())

View File

@@ -221,6 +221,8 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
cfn = kwargs.get('cfn', None)
expect = kwargs.get('expect', None)
answer = kwargs.get('answer', None)
options = kwargs.get('options', None)
cfn_extra_args = kwargs.get('cfn_extra_args', None)
# Create the response element
response_element = etree.Element("customresponse")
@@ -235,6 +237,33 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer_element = etree.SubElement(response_element, "answer")
answer_element.text = str(answer)
if options:
response_element.set('options', str(options))
if cfn_extra_args:
response_element.set('cfn_extra_args', str(cfn_extra_args))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class SymbolicResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating <symbolicresponse> XML trees """
def create_response_element(self, **kwargs):
cfn = kwargs.get('cfn', None)
answer = kwargs.get('answer', None)
options = kwargs.get('options', None)
response_element = etree.Element("symbolicresponse")
if cfn:
response_element.set('cfn', str(cfn))
if answer:
response_element.set('answer', str(answer))
if options:
response_element.set('options', str(options))
return response_element
def create_input_element(self, **kwargs):
@@ -638,12 +667,16 @@ class StringResponseXMLFactory(ResponseXMLFactory):
Where *hint_prompt* is the string for which we show the hint,
*hint_name* is an internal identifier for the hint,
and *hint_text* is the text we show for the hint.
*hintfn*: The name of a function in the script to use for hints.
"""
# Retrieve the **kwargs
answer = kwargs.get("answer", None)
case_sensitive = kwargs.get("case_sensitive", True)
hint_list = kwargs.get('hints', None)
assert(answer)
hint_fn = kwargs.get('hintfn', None)
assert answer
# Create the <stringresponse> element
response_element = etree.Element("stringresponse")
@@ -655,18 +688,24 @@ class StringResponseXMLFactory(ResponseXMLFactory):
response_element.set("type", "cs" if case_sensitive else "ci")
# Add the hints if specified
if hint_list:
if hint_list or hint_fn:
hintgroup_element = etree.SubElement(response_element, "hintgroup")
for (hint_prompt, hint_name, hint_text) in hint_list:
stringhint_element = etree.SubElement(hintgroup_element, "stringhint")
stringhint_element.set("answer", str(hint_prompt))
stringhint_element.set("name", str(hint_name))
if hint_list:
assert not hint_fn
for (hint_prompt, hint_name, hint_text) in hint_list:
stringhint_element = etree.SubElement(hintgroup_element, "stringhint")
stringhint_element.set("answer", str(hint_prompt))
stringhint_element.set("name", str(hint_name))
hintpart_element = etree.SubElement(hintgroup_element, "hintpart")
hintpart_element.set("on", str(hint_name))
hintpart_element = etree.SubElement(hintgroup_element, "hintpart")
hintpart_element.set("on", str(hint_name))
hint_text_element = etree.SubElement(hintpart_element, "text")
hint_text_element.text = str(hint_text)
hint_text_element = etree.SubElement(hintpart_element, "text")
hint_text_element.text = str(hint_text)
if hint_fn:
assert not hint_list
hintgroup_element.set("hintfn", hint_fn)
return response_element
@@ -705,3 +744,38 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
option_element.text = description
return input_element
class SymbolicResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <symbolicresponse> xml """
def create_response_element(self, **kwargs):
""" Build the <symbolicresponse> XML element.
Uses **kwargs:
*expect*: The correct answer (a sympy string)
*options*: list of option strings to pass to symmath_check
(e.g. 'matrix', 'qbit', 'imaginary', 'numerical')"""
# Retrieve **kwargs
expect = kwargs.get('expect', '')
options = kwargs.get('options', [])
# Symmath check expects a string of options
options_str = ",".join(options)
# Construct the <symbolicresponse> element
response_element = etree.Element('symbolicresponse')
if expect:
response_element.set('expect', str(expect))
if options_str:
response_element.set('options', str(options_str))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)

View File

@@ -26,7 +26,7 @@ class HelperTest(unittest.TestCase):
Make sure that our helper function works!
'''
def check(self, d):
xml = etree.XML(test_system.render_template('blah', d))
xml = etree.XML(test_system().render_template('blah', d))
self.assertEqual(d, extract_context(xml))
def test_extract_context(self):
@@ -46,11 +46,11 @@ class SolutionRenderTest(unittest.TestCase):
xml_str = """<solution id="solution_12">{s}</solution>""".format(s=solution)
element = etree.fromstring(xml_str)
renderer = lookup_tag('solution')(test_system, element)
renderer = lookup_tag('solution')(test_system(), element)
self.assertEqual(renderer.id, 'solution_12')
# our test_system "renders" templates to a div with the repr of the context
# Our test_system "renders" templates to a div with the repr of the context.
xml = renderer.get_html()
context = extract_context(xml)
self.assertEqual(context, {'id': 'solution_12'})
@@ -65,7 +65,7 @@ class MathRenderTest(unittest.TestCase):
xml_str = """<math>{tex}</math>""".format(tex=latex_in)
element = etree.fromstring(xml_str)
renderer = lookup_tag('math')(test_system, element)
renderer = lookup_tag('math')(test_system(), element)
self.assertEqual(renderer.mathstr, mathjax_out)

View File

@@ -0,0 +1,480 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/xhtml-math11-f.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta content="application/xhtml+xml; charset=UTF-8" http-equiv="Content-Type" />
<meta content="SnuggleTeX" name="Generator" />
<meta content="SnuggleTeX Documentation" name="description" />
<meta content="David McKain" name="author" />
<meta content="The University of Edinburgh" name="publisher" />
<link href="/snuggletex-webapp-1.2.2/includes/core.css" rel="stylesheet" />
<link href="/snuggletex-webapp-1.2.2/includes/webapp.css" rel="stylesheet" />
<link href="/snuggletex-webapp-1.2.2/includes/snuggletex.css" rel="stylesheet" />
<link href="/snuggletex-webapp-1.2.2/includes/jquery-ui-1.7.2.custom.css"
rel="stylesheet" /><script src="/snuggletex-webapp-1.2.2/includes/jquery.js" type="text/javascript"></script><script src="/snuggletex-webapp-1.2.2/includes/jquery-ui-1.7.2.custom.js"
type="text/javascript"></script><script src="/snuggletex-webapp-1.2.2/includes/webapp.js" type="text/javascript"></script><title>SnuggleTeX - ASCIIMathML Enrichment Demo</title><script src="/snuggletex-webapp-1.2.2/includes/ASCIIMathML.js" type="text/javascript"></script><script src="/snuggletex-webapp-1.2.2/includes/ASCIIMathMLwidget.js"
type="text/javascript"></script></head>
<body id="asciiMathMLUpConversionDemo">
<table border="0" cellpadding="0" cellspacing="0" id="header" width="100%">
<tr>
<td align="left" id="logo" valign="top"><a class="headertext" href="http://www.ed.ac.uk"><img alt="The University of Edinburgh" height="84"
src="/snuggletex-webapp-1.2.2/includes/uoe_logo.jpg"
width="84" /></a></td>
<td align="left">
<h3>THE UNIVERSITY of EDINBURGH</h3>
<h1>SCHOOL OF PHYSICS AND ASTRONOMY</h1>
</td>
</tr>
</table>
<h1 id="location"><a href="/snuggletex-webapp-1.2.2">SnuggleTeX (1.2.2)</a></h1>
<div id="content">
<div id="skipnavigation"><a href="#maincontent">Skip Navigation</a></div>
<div id="navigation">
<div id="navinner">
<h2>About SnuggleTeX</h2>
<ul>
<li><a href="/snuggletex-webapp-1.2.2/documentation/overview-and-features.html">Overview &amp; Features</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/use-cases.html">Why Use SnuggleTeX?</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/license.html">License</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/release-notes.html">Release Notes</a></li>
</ul>
<h2>Demos &amp; Samples</h2>
<ul>
<li><a href="/snuggletex-webapp-1.2.2/MathInputDemo">Simple Math Input Demo</a></li>
<li><a href="/snuggletex-webapp-1.2.2/FullLaTeXInputDemo">Full LaTeX Input Demo</a></li>
<li><a href="/snuggletex-webapp-1.2.2/UpConversionDemo">MathML Semantic Enrichment Demo</a></li>
<li><a class="selected" href="/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo">ASCIIMathML Enrichment Demo</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/web-output-samples.html">Web Output Samples</a></li>
</ul>
<h2>User Guide</h2>
<ul>
<li><a href="/snuggletex-webapp-1.2.2/documentation/getting-snuggletex.html">Getting SnuggleTeX</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/software-requirements.html">Software Requirements</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/your-classpath.html">Setting up Your ClassPath</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/examples.html">Examples</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/basic-usage.html">Basic Usage</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/inputs.html">Parsing LaTeX Inputs</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/xml-or-dom-output.html">Creating XML String or DOM Outputs</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/web-output.html">Creating Web Pages</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/error-reporting.html">Error Reporting</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/supported-latex.html">Supported LaTeX</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/advanced-usage.html">Advanced Usage</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/semantic-enrichment.html">Semantic Enrichment</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/migrating-from-older-versions.html">Migrating from older versions</a></li>
<li><a href="http://snuggletex.sourceforge.net/maven/apidocs/index.html">API Documentation<span class="extlink"> </span></a></li>
<li><a href="http://snuggletex.sourceforge.net/maven/xref/index.html">Source Code Cross-Reference<span class="extlink"> </span></a></li>
</ul>
<h2>SnuggleTeX Project Links</h2>
<ul>
<li><a href="http://sourceforge.net/project/showfiles.php?group_id=221375">Download from SourceForge.net<span class="extlink"> </span></a></li>
<li><a href="http://sourceforge.net/projects/snuggletex/">SnuggleTeX on SourceForge.net<span class="extlink"> </span></a></li>
<li><a href="http://snuggletex.sourceforge.net/maven/">SnuggleTeX Maven Developer Reports<span class="extlink"> </span></a></li>
<li><a href="https://www.wiki.ed.ac.uk/display/Physics/SnuggleTeX">SnuggleTeX Wiki<span class="extlink"> </span></a></li>
</ul>
</div>
</div>
<div id="maincontent">
<div id="popup"></div>
<div id="maininner">
<h2>ASCIIMathML Enrichment Demo</h2>
<h3>Input</h3>
<p>
This demo is similar to the
<a href="/snuggletex-webapp-1.2.2/UpConversionDemo">MathML Semantic Enrichnment Demo</a>
but uses
<a href="http://www1.chapman.edu/~jipsen/asciimath.html">ASCIIMathML</a> as
an alternative input format, which provides real-time feedback as you
type but can often generate MathML with odd semantics in it.
SnuggleTeX includes some functionality that can to convert this raw MathML into
something equivalent to its own MathML output, thereby allowing you to
<a href="/snuggletex-webapp-1.2.2/documentation/semantic-enrichment.html">semantically enrich</a> it in
certain simple cases, making ASCIIMathML a possibly viable input format
for simple semantic maths.
</p>
<p>
To try the demo, simply enter some some ASCIIMathML into the box below.
You should see a real time preview of this while you type.
Then hit <tt>Go!</tt> to use SnuggleTeX to semantically enrich your
input.
</p>
<form action="/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo" class="input"
method="post">
<div class="inputBox">
ASCIIMath Input:
<input id="asciiMathInput" name="asciiMathInput" type="text" value="" /><input id="asciiMathML" name="asciiMathML" type="hidden" /><input type="submit" value="Go!" /></div>
</form>
<h3>Live Preview</h3>
<p>
This is a MathML rendering of your input, generated by ASCIIMathML as you type.
</p>
<div class="result">
<div id="preview"> </div>
</div>
<p>
This is the underlying MathML source generated by ASCIIMathML, again updated in real time.
</p>
<div class="result"><pre id="previewSource"> </pre></div><script type="text/javascript">
registerASCIIMathMLInputWidget('asciiMathInput', 'preview', 'asciiMathML', 'previewSource');
var inputChanged = false;
// Hide any existing output stuff in page on first change, as it will no longer be in sync
jQuery(document).ready(function() {
jQuery('#asciiMathInput').bind('keydown', function() {
if (!inputChanged) jQuery('.outputContainer').css('visibility', 'hidden');
inputChanged = true;
});
});
</script><div class="outputContainer">
<h3>Enhanced Presentation MathML</h3>
<p>
This shows the result of attempting to enrich the raw Presentation MathML
generated by ASCIIMathML:
</p><pre class="result">&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;
&lt;mrow&gt;
&lt;mrow&gt;
&lt;mrow&gt;
&lt;mi&gt;cos&lt;/mi&gt;
&lt;mo&gt;&amp;ApplyFunction;&lt;/mo&gt;
&lt;mfenced close=")" open="("&gt;
&lt;mi&gt;theta&lt;/mi&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;mo&gt;&amp;sdot;&lt;/mo&gt;
&lt;mfenced close="]" open="["&gt;
&lt;mtable&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;/mtable&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;mo&gt;+&lt;/mo&gt;
&lt;mrow&gt;
&lt;mi&gt;i&lt;/mi&gt;
&lt;mo&gt;&amp;sdot;&lt;/mo&gt;
&lt;mrow&gt;
&lt;mi&gt;sin&lt;/mi&gt;
&lt;mo&gt;&amp;ApplyFunction;&lt;/mo&gt;
&lt;mfenced close=")" open="("&gt;
&lt;mi&gt;theta&lt;/mi&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;mo&gt;&amp;sdot;&lt;/mo&gt;
&lt;mfenced close="]" open="["&gt;
&lt;mtable&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;/mtable&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;/mrow&gt;
&lt;/math&gt;</pre><h3>Content MathML</h3>
<p>
This shows the result of an attempted
<a href="documentation/content-mathml.html">conversion to Content MathML</a>:
</p><pre class="result">&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;
&lt;apply&gt;
&lt;plus/&gt;
&lt;apply&gt;
&lt;times/&gt;
&lt;apply&gt;
&lt;cos/&gt;
&lt;ci&gt;theta&lt;/ci&gt;
&lt;/apply&gt;
&lt;list&gt;
&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;
&lt;/list&gt;
&lt;/apply&gt;
&lt;apply&gt;
&lt;times/&gt;
&lt;ci&gt;i&lt;/ci&gt;
&lt;apply&gt;
&lt;sin/&gt;
&lt;ci&gt;theta&lt;/ci&gt;
&lt;/apply&gt;
&lt;list&gt;
&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;
&lt;/list&gt;
&lt;/apply&gt;
&lt;/apply&gt;
&lt;/math&gt;</pre><h3>Maxima Input Form</h3>
<p>
This shows the result of an attempted
<a href="documentation/maxima-input.html">conversion to Maxima Input syntax</a>:
</p>
<p>
The conversion from Content MathML to Maxima Input was not successful for
this input.
</p>
<table class="failures">
<thead>
<tr>
<th>Failure Code</th>
<th>Message</th>
<th>XPath</th>
<th>Context</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/snuggletex-webapp-1.2.2/documentation/error-codes.html#UMFG00">UMFG00</a></td>
<td>Content MathML element matrix not supported</td>
<td>apply[1]/apply[1]/list[1]/matrix[1]</td>
<td><pre>&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;</pre></td>
</tr>
<tr>
<td><a href="/snuggletex-webapp-1.2.2/documentation/error-codes.html#UMFG00">UMFG00</a></td>
<td>Content MathML element matrix not supported</td>
<td>apply[1]/apply[2]/list[1]/matrix[1]</td>
<td><pre>&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;</pre></td>
</tr>
</tbody>
</table>
<h3>MathML Parallel Markup</h3>
<p>
This shows the enhanced Presentation MathML with other forms encapsulated
as annotations:
</p><pre class="result">&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;
&lt;semantics&gt;
&lt;mrow&gt;
&lt;mrow&gt;
&lt;mrow&gt;
&lt;mi&gt;cos&lt;/mi&gt;
&lt;mo&gt;&amp;ApplyFunction;&lt;/mo&gt;
&lt;mfenced close=")" open="("&gt;
&lt;mi&gt;theta&lt;/mi&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;mo&gt;&amp;sdot;&lt;/mo&gt;
&lt;mfenced close="]" open="["&gt;
&lt;mtable&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;/mtable&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;mo&gt;+&lt;/mo&gt;
&lt;mrow&gt;
&lt;mi&gt;i&lt;/mi&gt;
&lt;mo&gt;&amp;sdot;&lt;/mo&gt;
&lt;mrow&gt;
&lt;mi&gt;sin&lt;/mi&gt;
&lt;mo&gt;&amp;ApplyFunction;&lt;/mo&gt;
&lt;mfenced close=")" open="("&gt;
&lt;mi&gt;theta&lt;/mi&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;mo&gt;&amp;sdot;&lt;/mo&gt;
&lt;mfenced close="]" open="["&gt;
&lt;mtable&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;mtr&gt;
&lt;mtd&gt;
&lt;mn&gt;1&lt;/mn&gt;
&lt;/mtd&gt;
&lt;mtd&gt;
&lt;mn&gt;0&lt;/mn&gt;
&lt;/mtd&gt;
&lt;/mtr&gt;
&lt;/mtable&gt;
&lt;/mfenced&gt;
&lt;/mrow&gt;
&lt;/mrow&gt;
&lt;annotation-xml encoding="MathML-Content"&gt;
&lt;apply&gt;
&lt;plus/&gt;
&lt;apply&gt;
&lt;times/&gt;
&lt;apply&gt;
&lt;cos/&gt;
&lt;ci&gt;theta&lt;/ci&gt;
&lt;/apply&gt;
&lt;list&gt;
&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;
&lt;/list&gt;
&lt;/apply&gt;
&lt;apply&gt;
&lt;times/&gt;
&lt;ci&gt;i&lt;/ci&gt;
&lt;apply&gt;
&lt;sin/&gt;
&lt;ci&gt;theta&lt;/ci&gt;
&lt;/apply&gt;
&lt;list&gt;
&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;
&lt;/list&gt;
&lt;/apply&gt;
&lt;/apply&gt;
&lt;/annotation-xml&gt;
&lt;annotation encoding="ASCIIMathInput"/&gt;
&lt;annotation-xml encoding="Maxima-upconversion-failures"&gt;
&lt;s:fail xmlns:s="http://www.ph.ed.ac.uk/snuggletex" code="UMFG00"
message="Content MathML element matrix not supported"&gt;
&lt;s:arg&gt;matrix&lt;/s:arg&gt;
&lt;s:xpath&gt;apply[1]/apply[1]/list[1]/matrix[1]&lt;/s:xpath&gt;
&lt;s:context&gt;
&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;
&lt;/s:context&gt;
&lt;/s:fail&gt;
&lt;s:fail xmlns:s="http://www.ph.ed.ac.uk/snuggletex" code="UMFG00"
message="Content MathML element matrix not supported"&gt;
&lt;s:arg&gt;matrix&lt;/s:arg&gt;
&lt;s:xpath&gt;apply[1]/apply[2]/list[1]/matrix[1]&lt;/s:xpath&gt;
&lt;s:context&gt;
&lt;matrix&gt;
&lt;vector&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;/vector&gt;
&lt;vector&gt;
&lt;cn&gt;1&lt;/cn&gt;
&lt;cn&gt;0&lt;/cn&gt;
&lt;/vector&gt;
&lt;/matrix&gt;
&lt;/s:context&gt;
&lt;/s:fail&gt;
&lt;/annotation-xml&gt;
&lt;/semantics&gt;
&lt;/math&gt;</pre></div>
</div>
</div>
</div>
<div id="copyright">
<p>
SnuggleTeX Release 1.2.2 —
<a href="/snuggletex-webapp-1.2.2/documentation/release-notes.html">Release Notes</a><br />
Copyright © 2009
<a href="http://www.ph.ed.ac.uk">The School of Physics and Astronomy</a>,
<a href="http://www.ed.ac.uk">The University of Edinburgh</a>.
<br />
For more information, contact
<a href="http://www.ph.ed.ac.uk/elearning/contacts/#dmckain">David McKain</a>.
</p>
<p>
The University of Edinburgh is a charitable body, registered in Scotland,
with registration number SC005336.
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/xhtml-math11-f.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta content="application/xhtml+xml; charset=UTF-8" http-equiv="Content-Type" />
<meta content="SnuggleTeX" name="Generator" />
<meta content="SnuggleTeX Documentation" name="description" />
<meta content="David McKain" name="author" />
<meta content="The University of Edinburgh" name="publisher" />
<link href="/snuggletex-webapp-1.2.2/includes/core.css" rel="stylesheet" />
<link href="/snuggletex-webapp-1.2.2/includes/webapp.css" rel="stylesheet" />
<link href="/snuggletex-webapp-1.2.2/includes/snuggletex.css" rel="stylesheet" />
<link href="/snuggletex-webapp-1.2.2/includes/jquery-ui-1.7.2.custom.css"
rel="stylesheet" /><script src="/snuggletex-webapp-1.2.2/includes/jquery.js" type="text/javascript"></script><script src="/snuggletex-webapp-1.2.2/includes/jquery-ui-1.7.2.custom.js"
type="text/javascript"></script><script src="/snuggletex-webapp-1.2.2/includes/webapp.js" type="text/javascript"></script><title>SnuggleTeX - ASCIIMathML Enrichment Demo</title><script src="/snuggletex-webapp-1.2.2/includes/ASCIIMathML.js" type="text/javascript"></script><script src="/snuggletex-webapp-1.2.2/includes/ASCIIMathMLwidget.js"
type="text/javascript"></script></head>
<body id="asciiMathMLUpConversionDemo">
<table border="0" cellpadding="0" cellspacing="0" id="header" width="100%">
<tr>
<td align="left" id="logo" valign="top"><a class="headertext" href="http://www.ed.ac.uk"><img alt="The University of Edinburgh" height="84"
src="/snuggletex-webapp-1.2.2/includes/uoe_logo.jpg"
width="84" /></a></td>
<td align="left">
<h3>THE UNIVERSITY of EDINBURGH</h3>
<h1>SCHOOL OF PHYSICS AND ASTRONOMY</h1>
</td>
</tr>
</table>
<h1 id="location"><a href="/snuggletex-webapp-1.2.2">SnuggleTeX (1.2.2)</a></h1>
<div id="content">
<div id="skipnavigation"><a href="#maincontent">Skip Navigation</a></div>
<div id="navigation">
<div id="navinner">
<h2>About SnuggleTeX</h2>
<ul>
<li><a href="/snuggletex-webapp-1.2.2/documentation/overview-and-features.html">Overview &amp; Features</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/use-cases.html">Why Use SnuggleTeX?</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/license.html">License</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/release-notes.html">Release Notes</a></li>
</ul>
<h2>Demos &amp; Samples</h2>
<ul>
<li><a href="/snuggletex-webapp-1.2.2/MathInputDemo">Simple Math Input Demo</a></li>
<li><a href="/snuggletex-webapp-1.2.2/FullLaTeXInputDemo">Full LaTeX Input Demo</a></li>
<li><a href="/snuggletex-webapp-1.2.2/UpConversionDemo">MathML Semantic Enrichment Demo</a></li>
<li><a class="selected" href="/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo">ASCIIMathML Enrichment Demo</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/web-output-samples.html">Web Output Samples</a></li>
</ul>
<h2>User Guide</h2>
<ul>
<li><a href="/snuggletex-webapp-1.2.2/documentation/getting-snuggletex.html">Getting SnuggleTeX</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/software-requirements.html">Software Requirements</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/your-classpath.html">Setting up Your ClassPath</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/examples.html">Examples</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/basic-usage.html">Basic Usage</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/inputs.html">Parsing LaTeX Inputs</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/xml-or-dom-output.html">Creating XML String or DOM Outputs</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/web-output.html">Creating Web Pages</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/error-reporting.html">Error Reporting</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/supported-latex.html">Supported LaTeX</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/advanced-usage.html">Advanced Usage</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/semantic-enrichment.html">Semantic Enrichment</a></li>
<li><a href="/snuggletex-webapp-1.2.2/documentation/migrating-from-older-versions.html">Migrating from older versions</a></li>
<li><a href="http://snuggletex.sourceforge.net/maven/apidocs/index.html">API Documentation<span class="extlink"> </span></a></li>
<li><a href="http://snuggletex.sourceforge.net/maven/xref/index.html">Source Code Cross-Reference<span class="extlink"> </span></a></li>
</ul>
<h2>SnuggleTeX Project Links</h2>
<ul>
<li><a href="http://sourceforge.net/project/showfiles.php?group_id=221375">Download from SourceForge.net<span class="extlink"> </span></a></li>
<li><a href="http://sourceforge.net/projects/snuggletex/">SnuggleTeX on SourceForge.net<span class="extlink"> </span></a></li>
<li><a href="http://snuggletex.sourceforge.net/maven/">SnuggleTeX Maven Developer Reports<span class="extlink"> </span></a></li>
<li><a href="https://www.wiki.ed.ac.uk/display/Physics/SnuggleTeX">SnuggleTeX Wiki<span class="extlink"> </span></a></li>
</ul>
</div>
</div>
<div id="maincontent">
<div id="popup"></div>
<div id="maininner">
<h2>ASCIIMathML Enrichment Demo</h2>
<h3>Input</h3>
<p>
This demo is similar to the
<a href="/snuggletex-webapp-1.2.2/UpConversionDemo">MathML Semantic Enrichnment Demo</a>
but uses
<a href="http://www1.chapman.edu/~jipsen/asciimath.html">ASCIIMathML</a> as
an alternative input format, which provides real-time feedback as you
type but can often generate MathML with odd semantics in it.
SnuggleTeX includes some functionality that can to convert this raw MathML into
something equivalent to its own MathML output, thereby allowing you to
<a href="/snuggletex-webapp-1.2.2/documentation/semantic-enrichment.html">semantically enrich</a> it in
certain simple cases, making ASCIIMathML a possibly viable input format
for simple semantic maths.
</p>
<p>
To try the demo, simply enter some some ASCIIMathML into the box below.
You should see a real time preview of this while you type.
Then hit <tt>Go!</tt> to use SnuggleTeX to semantically enrich your
input.
</p>
<form action="/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo" class="input"
method="post">
<div class="inputBox">
ASCIIMath Input:
<input id="asciiMathInput" name="asciiMathInput" type="text" value="" /><input id="asciiMathML" name="asciiMathML" type="hidden" /><input type="submit" value="Go!" /></div>
</form>
<h3>Live Preview</h3>
<p>
This is a MathML rendering of your input, generated by ASCIIMathML as you type.
</p>
<div class="result">
<div id="preview"> </div>
</div>
<p>
This is the underlying MathML source generated by ASCIIMathML, again updated in real time.
</p>
<div class="result"><pre id="previewSource"> </pre></div><script type="text/javascript">
registerASCIIMathMLInputWidget('asciiMathInput', 'preview', 'asciiMathML', 'previewSource');
var inputChanged = false;
// Hide any existing output stuff in page on first change, as it will no longer be in sync
jQuery(document).ready(function() {
jQuery('#asciiMathInput').bind('keydown', function() {
if (!inputChanged) jQuery('.outputContainer').css('visibility', 'hidden');
inputChanged = true;
});
});
</script><div class="outputContainer">
<h3>Enhanced Presentation MathML</h3>
<p>
This shows the result of attempting to enrich the raw Presentation MathML
generated by ASCIIMathML:
</p><pre class="result">&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;
&lt;mn&gt;2&lt;/mn&gt;
&lt;/math&gt;</pre><h3>Content MathML</h3>
<p>
This shows the result of an attempted
<a href="documentation/content-mathml.html">conversion to Content MathML</a>:
</p><pre class="result">&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;
&lt;cn&gt;2&lt;/cn&gt;
&lt;/math&gt;</pre><h3>Maxima Input Form</h3>
<p>
This shows the result of an attempted
<a href="documentation/maxima-input.html">conversion to Maxima Input syntax</a>:
</p><pre class="result">2</pre><h3>MathML Parallel Markup</h3>
<p>
This shows the enhanced Presentation MathML with other forms encapsulated
as annotations:
</p><pre class="result">&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;
&lt;semantics&gt;
&lt;mn&gt;2&lt;/mn&gt;
&lt;annotation-xml encoding="MathML-Content"&gt;
&lt;cn&gt;2&lt;/cn&gt;
&lt;/annotation-xml&gt;
&lt;annotation encoding="ASCIIMathInput"/&gt;
&lt;annotation encoding="Maxima"&gt;2&lt;/annotation&gt;
&lt;/semantics&gt;
&lt;/math&gt;</pre></div>
</div>
</div>
</div>
<div id="copyright">
<p>
SnuggleTeX Release 1.2.2 —
<a href="/snuggletex-webapp-1.2.2/documentation/release-notes.html">Release Notes</a><br />
Copyright © 2009
<a href="http://www.ph.ed.ac.uk">The School of Physics and Astronomy</a>,
<a href="http://www.ed.ac.uk">The University of Edinburgh</a>.
<br />
For more information, contact
<a href="http://www.ph.ed.ac.uk/elearning/contacts/#dmckain">David McKain</a>.
</p>
<p>
The University of Edinburgh is a charitable body, registered in Scotland,
with registration number SC005336.
</p>
</div>
</body>
</html>

View File

@@ -6,12 +6,15 @@ import json
import mock
from capa.capa_problem import LoncapaProblem
from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from . import test_system
from . import test_system, new_loncapa_problem
class CapaHtmlRenderTest(unittest.TestCase):
def setUp(self):
super(CapaHtmlRenderTest, self).setUp()
self.system = test_system()
def test_blank_problem(self):
"""
It's important that blank problems don't break, since that's
@@ -20,7 +23,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
xml_str = "<problem> </problem>"
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -39,7 +42,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str, system=self.system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -49,9 +52,6 @@ class CapaHtmlRenderTest(unittest.TestCase):
self.assertEqual(test_element.tag, "test")
self.assertEqual(test_element.text, "Test include")
def test_process_outtext(self):
# Generate some XML with <startouttext /> and <endouttext />
xml_str = textwrap.dedent("""
@@ -61,7 +61,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -80,7 +80,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -98,7 +98,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
@@ -117,11 +117,12 @@ class CapaHtmlRenderTest(unittest.TestCase):
xml_str = StringResponseXMLFactory().build_xml(**kwargs)
# Mock out the template renderer
test_system.render_template = mock.Mock()
test_system.render_template.return_value = "<div>Input Template Render</div>"
the_system = test_system()
the_system.render_template = mock.Mock()
the_system.render_template.return_value = "<div>Input Template Render</div>"
# Create the problem and render the HTML
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str, system=the_system)
rendered_html = etree.XML(problem.get_html())
# Expect problem has been turned into a <div>
@@ -166,7 +167,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
mock.call('textline.html', expected_textline_context),
mock.call('solutionspan.html', expected_solution_context)]
self.assertEqual(test_system.render_template.call_args_list,
self.assertEqual(the_system.render_template.call_args_list,
expected_calls)
@@ -184,7 +185,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
xml_str = CustomResponseXMLFactory().build_xml(**kwargs)
# Create the problem and render the html
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Grade the problem
correctmap = problem.grade_answers({'1_2_1': 'test'})
@@ -219,7 +220,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem and render the HTML
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
rendered_html = etree.XML(problem.get_html())
# Expect that the variable $test has been replaced with its value
@@ -227,7 +228,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
self.assertEqual(span_element.get('attr'), "TEST")
def _create_test_file(self, path, content_str):
test_fp = test_system.filestore.open(path, "w")
test_fp = self.system.filestore.open(path, "w")
test_fp.write(content_str)
test_fp.close()

View File

@@ -45,7 +45,7 @@ class OptionInputTest(unittest.TestCase):
state = {'value': 'Down',
'id': 'sky_input',
'status': 'answered'}
option_input = lookup_tag('optioninput')(test_system, element, state)
option_input = lookup_tag('optioninput')(test_system(), element, state)
context = option_input._get_render_context()
@@ -92,7 +92,7 @@ class ChoiceGroupTest(unittest.TestCase):
'id': 'sky_input',
'status': 'answered'}
the_input = lookup_tag(tag)(test_system, element, state)
the_input = lookup_tag(tag)(test_system(), element, state)
context = the_input._get_render_context()
@@ -142,7 +142,7 @@ class JavascriptInputTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': '3', }
the_input = lookup_tag('javascriptinput')(test_system, element, state)
the_input = lookup_tag('javascriptinput')(test_system(), element, state)
context = the_input._get_render_context()
@@ -170,7 +170,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state)
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
@@ -198,7 +198,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state)
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
@@ -236,7 +236,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state)
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
@@ -274,7 +274,7 @@ class FileSubmissionTest(unittest.TestCase):
'status': 'incomplete',
'feedback': {'message': '3'}, }
input_class = lookup_tag('filesubmission')
the_input = input_class(test_system, element, state)
the_input = input_class(test_system(), element, state)
context = the_input._get_render_context()
@@ -319,7 +319,7 @@ class CodeInputTest(unittest.TestCase):
'feedback': {'message': '3'}, }
input_class = lookup_tag('codeinput')
the_input = input_class(test_system, element, state)
the_input = input_class(test_system(), element, state)
context = the_input._get_render_context()
@@ -368,7 +368,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
self.input_class = lookup_tag('matlabinput')
self.the_input = self.input_class(test_system, elt, state)
self.the_input = self.input_class(test_system(), elt, state)
def test_rendering(self):
context = self.the_input._get_render_context()
@@ -396,7 +396,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
@@ -423,7 +423,7 @@ class MatlabTest(unittest.TestCase):
}
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
@@ -448,7 +448,7 @@ class MatlabTest(unittest.TestCase):
}
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
@@ -470,7 +470,7 @@ class MatlabTest(unittest.TestCase):
get = {'submission': 'x = 1234;'}
response = self.the_input.handle_ajax("plot", get)
test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
test_system().xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
self.assertTrue(response['success'])
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
@@ -479,13 +479,12 @@ class MatlabTest(unittest.TestCase):
def test_plot_data_failure(self):
get = {'submission': 'x = 1234;'}
error_message = 'Error message!'
test_system.xqueue['interface'].send_to_queue.return_value = (1, error_message)
test_system().xqueue['interface'].send_to_queue.return_value = (1, error_message)
response = self.the_input.handle_ajax("plot", get)
self.assertFalse(response['success'])
self.assertEqual(response['message'], error_message)
self.assertTrue('queuekey' not in self.the_input.input_state)
self.assertTrue('queuestate' not in self.the_input.input_state)
test_system.xqueue['interface'].send_to_queue.return_value = (0, 'Success!')
def test_ungraded_response_success(self):
queuekey = 'abcd'
@@ -496,7 +495,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
@@ -514,7 +513,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
@@ -553,7 +552,7 @@ class SchematicTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('schematic')(test_system, element, state)
the_input = lookup_tag('schematic')(test_system(), element, state)
context = the_input._get_render_context()
@@ -592,7 +591,7 @@ class ImageInputTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('imageinput')(test_system, element, state)
the_input = lookup_tag('imageinput')(test_system(), element, state)
context = the_input._get_render_context()
@@ -643,7 +642,7 @@ class CrystallographyTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('crystallography')(test_system, element, state)
the_input = lookup_tag('crystallography')(test_system(), element, state)
context = the_input._get_render_context()
@@ -681,7 +680,7 @@ class VseprTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('vsepr_input')(test_system, element, state)
the_input = lookup_tag('vsepr_input')(test_system(), element, state)
context = the_input._get_render_context()
@@ -708,7 +707,7 @@ class ChemicalEquationTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'H2OYeah', }
self.the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
self.the_input = lookup_tag('chemicalequationinput')(test_system(), element, state)
def test_rendering(self):
''' Verify that the render context matches the expected render context'''
@@ -783,7 +782,7 @@ class DragAndDropTest(unittest.TestCase):
]
}
the_input = lookup_tag('drag_and_drop_input')(test_system, element, state)
the_input = lookup_tag('drag_and_drop_input')(test_system(), element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
@@ -832,7 +831,7 @@ class AnnotationInputTest(unittest.TestCase):
tag = 'annotationinput'
the_input = lookup_tag(tag)(test_system, element, state)
the_input = lookup_tag(tag)(test_system(), element, state)
context = the_input._get_render_context()

View File

@@ -2,7 +2,6 @@
Tests of responsetypes
"""
from datetime import datetime
import json
from nose.plugins.skip import SkipTest
@@ -10,10 +9,11 @@ import os
import random
import unittest
import textwrap
import mock
import textwrap
from . import test_system
from . import new_loncapa_problem, test_system
import capa.capa_problem as lcp
from capa.responsetypes import LoncapaProblemError, \
StudentInputError, ResponseError
from capa.correctmap import CorrectMap
@@ -30,9 +30,9 @@ class ResponseTest(unittest.TestCase):
if self.xml_factory_class:
self.xml_factory = self.xml_factory_class()
def build_problem(self, **kwargs):
def build_problem(self, system=None, **kwargs):
xml = self.xml_factory.build_xml(**kwargs)
return lcp.LoncapaProblem(xml, '1', system=test_system)
return new_loncapa_problem(xml, system=system)
def assert_grade(self, problem, submission, expected_correctness, msg=None):
input_dict = {'1_2_1': submission}
@@ -184,94 +184,151 @@ class ImageResponseTest(ResponseTest):
self.assert_answer_format(problem)
class SymbolicResponseTest(unittest.TestCase):
def test_sr_grade(self):
raise SkipTest() # This test fails due to dependencies on a local copy of snuggletex-webapp. Until we have figured that out, we'll just skip this test
symbolicresponse_file = os.path.dirname(__file__) + "/test_files/symbolicresponse.xml"
test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=test_system)
correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]',
'1_2_1_dynamath': '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>cos</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>+</mo>
<mi>i</mi>
<mo>&#x22C5;</mo>
<mrow>
<mi>sin</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</mstyle>
</math>
''',
}
wrong_answers = {'1_2_1': '2',
'1_2_1_dynamath': '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>2</mn>
</mstyle>
</math>''',
}
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 SymbolicResponseTest(ResponseTest):
from response_xml_factory import SymbolicResponseXMLFactory
xml_factory_class = SymbolicResponseXMLFactory
def test_grade_single_input(self):
problem = self.build_problem(math_display=True,
expect="2*x+3*y")
# Correct answers
correct_inputs = [
('2x+3y', textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>2</mn><mo>*</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
</mstyle></math>""")),
('x+x+3y', textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mi>x</mi><mo>+</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
</mstyle></math>""")),
]
for (input_str, input_mathml) in correct_inputs:
self._assert_symbolic_grade(problem, input_str, input_mathml, 'correct')
# Incorrect answers
incorrect_inputs = [
('0', ''),
('4x+3y', textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>4</mn><mo>*</mo><mi>x</mi><mo>+</mo><mn>3</mn><mo>*</mo><mi>y</mi>
</mstyle></math>""")),
]
for (input_str, input_mathml) in incorrect_inputs:
self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect')
def test_complex_number_grade(self):
problem = self.build_problem(math_display=True,
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
options=["matrix", "imaginary"])
# For LaTeX-style inputs, symmath_check() will try to contact
# a server to convert the input to MathML.
# We mock out the server, simulating the response that it would give
# for this input.
import requests
dirpath = os.path.dirname(__file__)
correct_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_correct.html")).read().decode('utf8')
wrong_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_wrong.html")).read().decode('utf8')
# Correct answer
with mock.patch.object(requests, 'post') as mock_post:
# Simulate what the LaTeX-to-MathML server would
# send for the correct response input
mock_post.return_value.text = correct_snuggletex_response
self._assert_symbolic_grade(problem,
"cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]",
textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>cos</mi>
<mrow><mo>(</mo><mi>&#x3B8;</mi><mo>)</mo></mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
</mtr>
<mtr>
<mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>+</mo>
<mi>i</mi>
<mo>&#x22C5;</mo>
<mrow>
<mi>sin</mi>
<mrow>
<mo>(</mo><mi>&#x3B8;</mi><mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd><mn>0</mn></mtd><mtd><mn>1</mn></mtd>
</mtr>
<mtr>
<mtd><mn>1</mn></mtd><mtd><mn>0</mn></mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</mstyle>
</math>
"""),
'correct')
# Incorrect answer
with mock.patch.object(requests, 'post') as mock_post:
# Simulate what the LaTeX-to-MathML server would
# send for the incorrect response input
mock_post.return_value.text = wrong_snuggletex_response
self._assert_symbolic_grade(problem, "2",
textwrap.dedent("""
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true"><mn>2</mn></mstyle>
</math>
"""),
'incorrect')
def test_multiple_inputs_exception(self):
# Should not allow multiple inputs, since we specify
# only one "expect" value
with self.assertRaises(Exception):
problem = self.build_problem(math_display=True,
expect="2*x+3*y",
num_inputs=3)
def _assert_symbolic_grade(self, problem,
student_input,
dynamath_input,
expected_correctness):
input_dict = {'1_2_1': str(student_input),
'1_2_1_dynamath': str(dynamath_input) }
correct_map = problem.grade_answers(input_dict)
self.assertEqual(correct_map.get_correctness('1_2_1'),
expected_correctness)
class OptionResponseTest(ResponseTest):
@@ -531,6 +588,22 @@ class StringResponseTest(ResponseTest):
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "")
def test_computed_hints(self):
problem = self.build_problem(
answer="Michigan",
hintfn="gimme_a_hint",
script = textwrap.dedent("""
def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap):
aid = answer_ids[0]
answer = student_answers[aid]
new_cmap.set_hint_and_mode(aid, answer+"??", "always")
""")
)
input_dict = {'1_2_1': 'Hello'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "Hello??")
class CodeResponseTest(ResponseTest):
from response_xml_factory import CodeResponseXMLFactory
@@ -708,18 +781,39 @@ class JavascriptResponseTest(ResponseTest):
def test_grade(self):
# Compile coffee files into javascript used by the response
coffee_file_path = os.path.dirname(__file__) + "/test_files/js/*.coffee"
os.system("coffee -c %s" % (coffee_file_path))
os.system("node_modules/.bin/coffee -c %s" % (coffee_file_path))
problem = self.build_problem(generator_src="test_problem_generator.js",
grader_src="test_problem_grader.js",
display_class="TestProblemDisplay",
display_src="test_problem_display.js",
param_dict={'value': '4'})
system = test_system()
system.can_execute_unsafe_code = lambda: True
problem = self.build_problem(
system=system,
generator_src="test_problem_generator.js",
grader_src="test_problem_grader.js",
display_class="TestProblemDisplay",
display_src="test_problem_display.js",
param_dict={'value': '4'},
)
# Test that we get graded correctly
self.assert_grade(problem, json.dumps({0: 4}), "correct")
self.assert_grade(problem, json.dumps({0: 5}), "incorrect")
def test_cant_execute_javascript(self):
# If the system says to disallow unsafe code execution, then making
# this problem will raise an exception.
system = test_system()
system.can_execute_unsafe_code = lambda: False
with self.assertRaises(LoncapaProblemError):
problem = self.build_problem(
system=system,
generator_src="test_problem_generator.js",
grader_src="test_problem_grader.js",
display_class="TestProblemDisplay",
display_src="test_problem_display.js",
param_dict={'value': '4'},
)
class NumericalResponseTest(ResponseTest):
from response_xml_factory import NumericalResponseXMLFactory
@@ -853,9 +947,8 @@ class CustomResponseTest(ResponseTest):
#
# 'answer_given' is the answer the student gave (if there is just one input)
# or an ordered list of answers (if there are multiple inputs)
#
#
# The function should return a dict of the form
#
# The function should return a dict of the form
# { 'ok': BOOL, 'msg': STRING }
#
script = textwrap.dedent("""
@@ -964,6 +1057,35 @@ class CustomResponseTest(ResponseTest):
self.assertEqual(correct_map.get_msg('1_2_2'), 'Feedback 2')
self.assertEqual(correct_map.get_msg('1_2_3'), 'Feedback 3')
def test_function_code_with_extra_args(self):
script = textwrap.dedent("""\
def check_func(expect, answer_given, options, dynamath):
assert options == "xyzzy", "Options was %r" % options
return {'ok': answer_given == expect, 'msg': 'Message text'}
""")
problem = self.build_problem(script=script, cfn="check_func", expect="42", options="xyzzy", cfn_extra_args="options dynamath")
# Correct answer
input_dict = {'1_2_1': '42'}
correct_map = problem.grade_answers(input_dict)
correctness = correct_map.get_correctness('1_2_1')
msg = correct_map.get_msg('1_2_1')
self.assertEqual(correctness, 'correct')
self.assertEqual(msg, "Message text")
# Incorrect answer
input_dict = {'1_2_1': '0'}
correct_map = problem.grade_answers(input_dict)
correctness = correct_map.get_correctness('1_2_1')
msg = correct_map.get_msg('1_2_1')
self.assertEqual(correctness, 'incorrect')
self.assertEqual(msg, "Message text")
def test_multiple_inputs_return_one_status(self):
# When given multiple inputs, the 'answer_given' argument
# to the check_func() is a list of inputs

View File

@@ -1,4 +1,4 @@
from .calc import evaluator, UndefinedVariable
from calc import evaluator, UndefinedVariable
from cmath import isinf
#-----------------------------------------------------------------------------

View File

@@ -4,5 +4,5 @@ setup(
name="capa",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute==0.6.30', 'pyparsing==1.5.6'],
install_requires=["distribute==0.6.28"],
)

View File

@@ -0,0 +1,13 @@
(Originally written by Ike.)
At a high level, the main challenges of checking symbolic math expressions are (1) making sure the expression is mathematically legal, and (2) simplifying the expression for comparison with what is expected.
(1) Generation (and testing) of legal input is done by using MathJax to provide input math in an XML format known as Presentation MathML (PMathML). Such expressions typeset correctly, but may not be mathematically legal, like "5 / (1 = 2)". The PMathML is converted into "Content MathML" (CMathML), which is by definition mathematically legal, using an XSLT 2.0 stylesheet, via a module in SnuggleTeX. CMathML is then converted into a sympy expression. This work is all done in `lms/lib/symmath/formula.py`
(2) Simplifying the expression and checking against what is expected is done by using sympy, and a set of heuristics based on options flags provided by the problem author. For example, the problem author may specify that the expected expression is a matrix, in which case the dimensionality of the input expression is checked. Other options include specifying that the comparison be checked numerically in addition to symbolically. The checking is done in stages, first with no simplification, then with increasing levels of testing; if a match is found at any stage, then an "ok" is returned. Helpful messages are also returned, eg if the input expression is of a different type than the expected. This work is all done in `lms/lib/symmath/symmath_check.py`
Links:
SnuggleTex: http://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html
MathML: http://www.w3.org/TR/MathML2/overview.html
SymPy: http://sympy.org/en/index.html

View File

@@ -0,0 +1,2 @@
from .formula import *
from .symmath_check import *

View File

@@ -0,0 +1,739 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# File: formula.py
# Date: 04-May-12 (creation)
# Author: I. Chuang <ichuang@mit.edu>
#
# flexible python representation of a symbolic mathematical formula.
# Acceptes Presentation MathML, Content MathML (and could also do OpenMath)
# Provides sympy representation.
import os
import sys
import string
import re
import logging
import operator
import sympy
from sympy.printing.latex import LatexPrinter
from sympy.printing.str import StrPrinter
from sympy import latex, sympify
from sympy.physics.quantum.qubit import *
from sympy.physics.quantum.state import *
# from sympy import exp, pi, I
# from sympy.core.operations import LatticeOp
# import sympy.physics.quantum.qubit
import urllib
from xml.sax.saxutils import escape, unescape
import sympy
import unicodedata
from lxml import etree
#import subprocess
import requests
from copy import deepcopy
log = logging.getLogger(__name__)
log.warning("Dark code. Needs review before enabling in prod.")
os.environ['PYTHONIOENCODING'] = 'utf-8'
#-----------------------------------------------------------------------------
class dot(sympy.operations.LatticeOp): # my dot product
zero = sympy.Symbol('dotzero')
identity = sympy.Symbol('dotidentity')
#class dot(sympy.Mul): # my dot product
# is_Mul = False
def _print_dot(self, expr):
return '{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1])
LatexPrinter._print_dot = _print_dot
#-----------------------------------------------------------------------------
# unit vectors (for 8.02)
def _print_hat(self, expr): return '\\hat{%s}' % str(expr.args[0]).lower()
LatexPrinter._print_hat = _print_hat
StrPrinter._print_hat = _print_hat
#-----------------------------------------------------------------------------
# helper routines
def to_latex(x):
if x is None: return ''
# LatexPrinter._print_dot = _print_dot
xs = latex(x)
xs = xs.replace(r'\XI', 'XI') # workaround for strange greek
# substitute back into latex form for scripts
# literally something of the form
# 'scriptN' becomes '\\mathcal{N}'
# note: can't use something akin to the _print_hat method above because we sometimes get 'script(N)__B' or more complicated terms
xs = re.sub(r'script([a-zA-Z0-9]+)',
'\\mathcal{\\1}',
xs)
#return '<math>%s{}{}</math>' % (xs[1:-1])
if xs[0] == '$':
return '[mathjax]%s[/mathjax]<br>' % (xs[1:-1]) # for sympy v6
return '[mathjax]%s[/mathjax]<br>' % (xs) # for sympy v7
def my_evalf(expr, chop=False):
if type(expr) == list:
try:
return [x.evalf(chop=chop) for x in expr]
except:
return expr
try:
return expr.evalf(chop=chop)
except:
return expr
#-----------------------------------------------------------------------------
# my version of sympify to import expression into sympy
def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False, symtab=None):
# make all lowercase real?
if symtab:
varset = symtab
else:
varset = {'p': sympy.Symbol('p'),
'g': sympy.Symbol('g'),
'e': sympy.E, # for exp
'i': sympy.I, # lowercase i is also sqrt(-1)
'Q': sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
'N': sympy.Symbol('N'), # or it is some kind of sympy function
#'X':sympy.sympify('Matrix([[0,1],[1,0]])'),
#'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'),
#'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'),
'ZZ': sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing
'XI': sympy.Symbol('XI'), # otherwise it is the capital \XI
'hat': sympy.Function('hat'), # for unit vectors (8.02)
}
if do_qubit: # turn qubit(...) into Qubit instance
varset.update({'qubit': sympy.physics.quantum.qubit.Qubit,
'Ket': sympy.physics.quantum.state.Ket,
'dot': dot,
'bit': sympy.Function('bit'),
})
if abcsym: # consider all lowercase letters as real symbols, in the parsing
for letter in string.lowercase:
if letter in varset: # exclude those already done
continue
varset.update({letter: sympy.Symbol(letter, real=True)})
sexpr = sympify(expr, locals=varset)
if normphase: # remove overall phase if sexpr is a list
if type(sexpr) == list:
if sexpr[0].is_number:
ophase = sympy.sympify('exp(-I*arg(%s))' % sexpr[0])
sexpr = [sympy.Mul(x, ophase) for x in sexpr]
def to_matrix(x): # if x is a list of lists, and is rectangular, then return Matrix(x)
if not type(x) == list:
return x
for row in x:
if (not type(row) == list):
return x
rdim = len(x[0])
for row in x:
if not len(row) == rdim:
return x
return sympy.Matrix(x)
if matrix:
sexpr = to_matrix(sexpr)
return sexpr
#-----------------------------------------------------------------------------
# class for symbolic mathematical formulas
class formula(object):
'''
Representation of a mathematical formula object. Accepts mathml math expression
for constructing, and can produce sympy translation. The formula may or may not
include an assignment (=).
'''
def __init__(self, expr, asciimath='', options=None):
self.expr = expr.strip()
self.asciimath = asciimath
self.the_cmathml = None
self.the_sympy = None
self.options = options
def is_presentation_mathml(self):
return '<mstyle' in self.expr
def is_mathml(self):
return '<math ' in self.expr
def fix_greek_in_mathml(self, xml):
def gettag(x):
return re.sub('{http://[^}]+}', '', x.tag)
for k in xml:
tag = gettag(k)
if tag == 'mi' or tag == 'ci':
usym = unicode(k.text)
try:
udata = unicodedata.name(usym)
except Exception, err:
udata = None
#print "usym = %s, udata=%s" % (usym,udata)
if udata: # eg "GREEK SMALL LETTER BETA"
if 'GREEK' in udata:
usym = udata.split(' ')[-1]
if 'SMALL' in udata: usym = usym.lower()
#print "greek: ",usym
k.text = usym
self.fix_greek_in_mathml(k)
return xml
def preprocess_pmathml(self, xml):
'''
Pre-process presentation MathML from ASCIIMathML to make it more
acceptable for SnuggleTeX, and also to accomodate some sympy
conventions (eg hat(i) for \hat{i}).
This method would be a good spot to look for an integral and convert
it, if possible...
'''
if type(xml) == str or type(xml) == unicode:
xml = etree.fromstring(xml) # TODO: wrap in try
xml = self.fix_greek_in_mathml(xml) # convert greek utf letters to greek spelled out in ascii
def gettag(x):
return re.sub('{http://[^}]+}', '', x.tag)
# f and g are processed as functions by asciimathml, eg "f-2" turns into "<mrow><mi>f</mi><mo>-</mo></mrow><mn>2</mn>"
# this is really terrible for turning into cmathml.
# undo this here.
def fix_pmathml(xml):
for k in xml:
tag = gettag(k)
if tag == 'mrow':
if len(k) == 2:
if gettag(k[0]) == 'mi' and k[0].text in ['f', 'g'] and gettag(k[1]) == 'mo':
idx = xml.index(k)
xml.insert(idx, deepcopy(k[0])) # drop the <mrow> container
xml.insert(idx + 1, deepcopy(k[1]))
xml.remove(k)
fix_pmathml(k)
fix_pmathml(xml)
# hat i is turned into <mover><mi>i</mi><mo>^</mo></mover> ; mangle this into <mi>hat(f)</mi>
# hat i also somtimes turned into <mover><mrow> <mi>j</mi> </mrow><mo>^</mo></mover>
def fix_hat(xml):
for k in xml:
tag = gettag(k)
if tag == 'mover':
if len(k) == 2:
if gettag(k[0]) == 'mi' and gettag(k[1]) == 'mo' and str(k[1].text) == '^':
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0].text
xml.replace(k, newk)
if gettag(k[0]) == 'mrow' and gettag(k[0][0]) == 'mi' and gettag(k[1]) == 'mo' and str(k[1].text) == '^':
newk = etree.Element('mi')
newk.text = 'hat(%s)' % k[0][0].text
xml.replace(k, newk)
fix_hat(k)
fix_hat(xml)
def flatten_pmathml(xml):
''' Give the text version of certain PMathML elements
Sometimes MathML will be given with each letter separated (it
doesn't know if its implicit multiplication or what). From an xml
node, find the (text only) variable name it represents. So it takes
<mrow>
<mi>m</mi>
<mi>a</mi>
<mi>x</mi>
</mrow>
and returns 'max', for easier use later on.
'''
tag = gettag(xml)
if tag == 'mn': return xml.text
elif tag == 'mi': return xml.text
elif tag == 'mrow': return ''.join([flatten_pmathml(y) for y in xml])
raise Exception, '[flatten_pmathml] unknown tag %s' % tag
def fix_mathvariant(parent):
'''Fix certain kinds of math variants
Literally replace <mstyle mathvariant="script"><mi>N</mi></mstyle>
with 'scriptN'. There have been problems using script_N or script(N)
'''
for child in parent:
if (gettag(child) == 'mstyle' and child.get('mathvariant') == 'script'):
newchild = etree.Element('mi')
newchild.text = 'script%s' % flatten_pmathml(child[0])
parent.replace(child, newchild)
fix_mathvariant(child)
fix_mathvariant(xml)
# find "tagged" superscripts
# they have the character \u200b in the superscript
# replace them with a__b so snuggle doesn't get confused
def fix_superscripts(xml):
''' Look for and replace sup elements with 'X__Y' or 'X_Y__Z'
In the javascript, variables with '__X' in them had an invisible
character inserted into the sup (to distinguish from powers)
E.g. normal:
<msubsup>
<mi>a</mi>
<mi>b</mi>
<mi>c</mi>
</msubsup>
to be interpreted '(a_b)^c' (nothing done by this method)
And modified:
<msubsup>
<mi>b</mi>
<mi>x</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>d</mi>
</mrow>
</msubsup>
to be interpreted 'a_b__c'
also:
<msup>
<mi>x</mi>
<mrow>
<mo>&#x200B;</mo>
<mi>B</mi>
</mrow>
</msup>
to be 'x__B'
'''
for k in xml:
tag = gettag(k)
# match things like the last example--
# the second item in msub is an mrow with the first
# character equal to \u200b
if (tag == 'msup' and
len(k) == 2 and gettag(k[1]) == 'mrow' and
gettag(k[1][0]) == 'mo' and k[1][0].text == u'\u200b'): # whew
# replace the msup with 'X__Y'
k[1].remove(k[1][0])
newk = etree.Element('mi')
newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]))
xml.replace(k, newk)
# match things like the middle example-
# the third item in msubsup is an mrow with the first
# character equal to \u200b
if (tag == 'msubsup' and
len(k) == 3 and gettag(k[2]) == 'mrow' and
gettag(k[2][0]) == 'mo' and k[2][0].text == u'\u200b'): # whew
# replace the msubsup with 'X_Y__Z'
k[2].remove(k[2][0])
newk = etree.Element('mi')
newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2]))
xml.replace(k, newk)
fix_superscripts(k)
fix_superscripts(xml)
# Snuggle returns an error when it sees an <msubsup>
# replace such elements with an <msup>, except the first element is of
# the form a_b. I.e. map a_b^c => (a_b)^c
def fix_msubsup(parent):
for child in parent:
# fix msubsup
if (gettag(child) == 'msubsup' and len(child) == 3):
newchild = etree.Element('msup')
newbase = etree.Element('mi')
newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1]))
newexp = child[2]
newchild.append(newbase)
newchild.append(newexp)
parent.replace(child, newchild)
fix_msubsup(child)
fix_msubsup(xml)
self.xml = xml
return self.xml
def get_content_mathml(self):
if self.the_cmathml: return self.the_cmathml
# pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
try:
xml = self.preprocess_pmathml(self.expr)
except Exception, err:
log.warning('Err %s while preprocessing; expr=%s' % (err, self.expr))
return "<html>Error! Cannot process pmathml</html>"
pmathml = etree.tostring(xml, pretty_print=True)
self.the_pmathml = pmathml
# convert to cmathml
self.the_cmathml = self.GetContentMathML(self.asciimath, pmathml)
return self.the_cmathml
cmathml = property(get_content_mathml, None, None, 'content MathML representation')
def make_sympy(self, xml=None):
'''
Return sympy expression for the math formula.
The math formula is converted to Content MathML then that is parsed.
This is a recursive function, called on every CMML node. Support for
more functions can be added by modifying opdict, abould halfway down
'''
if self.the_sympy: return self.the_sympy
if xml is None: # root
if not self.is_mathml():
return my_sympify(self.expr)
if self.is_presentation_mathml():
cmml = None
try:
cmml = self.cmathml
xml = etree.fromstring(str(cmml))
except Exception, err:
if 'conversion from Presentation MathML to Content MathML was not successful' in cmml:
msg = "Illegal math expression"
else:
msg = 'Err %s while converting cmathml to xml; cmml=%s' % (err, cmml)
raise Exception, msg
xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0])
else:
xml = etree.fromstring(self.expr)
xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0])
return self.the_sympy
def gettag(x):
return re.sub('{http://[^}]+}', '', x.tag)
# simple math
def op_divide(*args):
if not len(args) == 2:
raise Exception, 'divide given wrong number of arguments!'
# print "divide: arg0=%s, arg1=%s" % (args[0],args[1])
return sympy.Mul(args[0], sympy.Pow(args[1], -1))
def op_plus(*args): return args[0] if len(args) == 1 else op_plus(*args[:-1]) + args[-1]
def op_times(*args): return reduce(operator.mul, args)
def op_minus(*args):
if len(args) == 1:
return -args[0]
if not len(args) == 2:
raise Exception, 'minus given wrong number of arguments!'
#return sympy.Add(args[0],-args[1])
return args[0] - args[1]
opdict = {'plus': op_plus,
'divide': operator.div,
'times': op_times,
'minus': op_minus,
#'plus': sympy.Add,
#'divide' : op_divide,
#'times' : sympy.Mul,
'minus': op_minus,
'root': sympy.sqrt,
'power': sympy.Pow,
'sin': sympy.sin,
'cos': sympy.cos,
'tan': sympy.tan,
'cot': sympy.cot,
'sinh': sympy.sinh,
'cosh': sympy.cosh,
'coth': sympy.coth,
'tanh': sympy.tanh,
'asin': sympy.asin,
'acos': sympy.acos,
'atan': sympy.atan,
'atan2': sympy.atan2,
'acot': sympy.acot,
'asinh': sympy.asinh,
'acosh': sympy.acosh,
'atanh': sympy.atanh,
'acoth': sympy.acoth,
'exp': sympy.exp,
'log': sympy.log,
'ln': sympy.ln,
}
# simple sumbols
nums1dict = {'pi': sympy.pi,
}
def parsePresentationMathMLSymbol(xml):
'''
Parse <msub>, <msup>, <mi>, and <mn>
'''
tag = gettag(xml)
if tag == 'mn': return xml.text
elif tag == 'mi': return xml.text
elif tag == 'msub': return '_'.join([parsePresentationMathMLSymbol(y) for y in xml])
elif tag == 'msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml])
raise Exception, '[parsePresentationMathMLSymbol] unknown tag %s' % tag
# parser tree for Content MathML
tag = gettag(xml)
# print "tag = ",tag
# first do compound objects
if tag == 'apply': # apply operator
opstr = gettag(xml[0])
if opstr in opdict:
op = opdict[opstr]
args = [self.make_sympy(x) for x in xml[1:]]
try:
res = op(*args)
except Exception, err:
self.args = args
self.op = op
raise Exception, '[formula] error=%s failed to apply %s to args=%s' % (err, opstr, args)
return res
else:
raise Exception, '[formula]: unknown operator tag %s' % (opstr)
elif tag == 'list': # square bracket list
if gettag(xml[0]) == 'matrix':
return self.make_sympy(xml[0])
else:
return [self.make_sympy(x) for x in xml]
elif tag == 'matrix':
return sympy.Matrix([self.make_sympy(x) for x in xml])
elif tag == 'vector':
return [self.make_sympy(x) for x in xml]
# atoms are below
elif tag == 'cn': # number
return sympy.sympify(xml.text)
return float(xml.text)
elif tag == 'ci': # variable (symbol)
if len(xml) > 0 and (gettag(xml[0]) == 'msub' or gettag(xml[0]) == 'msup'): # subscript or superscript
usym = parsePresentationMathMLSymbol(xml[0])
sym = sympy.Symbol(str(usym))
else:
usym = unicode(xml.text)
if 'hat' in usym:
sym = my_sympify(usym)
else:
if usym == 'i' and self.options is not None and 'imaginary' in self.options: # i = sqrt(-1)
sym = sympy.I
else:
sym = sympy.Symbol(str(usym))
return sym
else: # unknown tag
raise Exception, '[formula] unknown tag %s' % tag
sympy = property(make_sympy, None, None, 'sympy representation')
def GetContentMathML(self, asciimath, mathml):
# URL = 'http://192.168.1.2:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
# URL = 'http://127.0.0.1:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
URL = 'https://math-xserver.mitx.mit.edu/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
if 1:
payload = {'asciiMathInput': asciimath,
'asciiMathML': mathml,
#'asciiMathML':unicode(mathml).encode('utf-8'),
}
headers = {'User-Agent': "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"}
r = requests.post(URL, data=payload, headers=headers, verify=False)
r.encoding = 'utf-8'
ret = r.text
#print "encoding: ",r.encoding
# return ret
mode = 0
cmathml = []
for k in ret.split('\n'):
if 'conversion to Content MathML' in k:
mode = 1
continue
if mode == 1:
if '<h3>Maxima Input Form</h3>' in k:
mode = 0
continue
cmathml.append(k)
# return '\n'.join(cmathml)
cmathml = '\n'.join(cmathml[2:])
cmathml = '<math xmlns="http://www.w3.org/1998/Math/MathML">\n' + unescape(cmathml) + '\n</math>'
# print cmathml
#return unicode(cmathml)
return cmathml
#-----------------------------------------------------------------------------
def test1():
xmlstr = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<plus/>
<cn>1</cn>
<cn>2</cn>
</apply>
</math>
'''
return formula(xmlstr)
def test2():
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<plus/>
<cn>1</cn>
<apply>
<times/>
<cn>2</cn>
<ci>α</ci>
</apply>
</apply>
</math>
'''
return formula(xmlstr)
def test3():
xmlstr = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<apply>
<divide/>
<cn>1</cn>
<apply>
<plus/>
<cn>2</cn>
<ci>γ</ci>
</apply>
</apply>
</math>
'''
return formula(xmlstr)
def test4():
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>1</mn>
<mo>+</mo>
<mfrac>
<mn>2</mn>
<mi>α</mi>
</mfrac>
</mstyle>
</math>
'''
return formula(xmlstr)
def test5(): # sum of two matrices
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>cos</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>+</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</mstyle>
</math>
'''
return formula(xmlstr)
def test6(): # imaginary numbers
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>1</mn>
<mo>+</mo>
<mi>i</mi>
</mstyle>
</math>
'''
return formula(xmlstr, options='imaginary')

View File

@@ -0,0 +1,328 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# File: symmath_check.py
# Date: 02-May-12 (creation)
#
# Symbolic mathematical expression checker for edX. Uses sympy to check for expression equality.
#
# Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX
import os
import sys
import string
import re
import traceback
from .formula import *
import logging
log = logging.getLogger(__name__)
#-----------------------------------------------------------------------------
# check function interface
#
# This is one of the main entry points to call.
def symmath_check_simple(expect, ans, adict={}, symtab=None, extra_options=None):
'''
Check a symbolic mathematical expression using sympy.
The input is an ascii string (not MathML) converted to math using sympy.sympify.
'''
options = {'__MATRIX__': False, '__ABC__': False, '__LOWER__': False}
if extra_options: options.update(extra_options)
for op in options: # find options in expect string
if op in expect:
expect = expect.replace(op, '')
options[op] = True
expect = expect.replace('__OR__', '__or__') # backwards compatibility
if options['__LOWER__']:
expect = expect.lower()
ans = ans.lower()
try:
ret = check(expect, ans,
matrix=options['__MATRIX__'],
abcsym=options['__ABC__'],
symtab=symtab,
)
except Exception, err:
return {'ok': False,
'msg': 'Error %s<br/>Failed in evaluating check(%s,%s)' % (err, expect, ans)
}
return ret
#-----------------------------------------------------------------------------
# pretty generic checking function
def check(expect, given, numerical=False, matrix=False, normphase=False, abcsym=False, do_qubit=True, symtab=None, dosimplify=False):
"""
Returns dict with
'ok': True if check is good, False otherwise
'msg': response message (in HTML)
"expect" may have multiple possible acceptable answers, separated by "__OR__"
"""
if "__or__" in expect: # if multiple acceptable answers
eset = expect.split('__or__') # then see if any match
for eone in eset:
ret = check(eone, given, numerical, matrix, normphase, abcsym, do_qubit, symtab, dosimplify)
if ret['ok']:
return ret
return ret
flags = {}
if "__autonorm__" in expect:
flags['autonorm'] = True
expect = expect.replace('__autonorm__', '')
matrix = True
threshold = 1.0e-3
if "__threshold__" in expect:
(expect, st) = expect.split('__threshold__')
threshold = float(st)
numerical = True
if str(given) == '' and not (str(expect) == ''):
return {'ok': False, 'msg': ''}
try:
xgiven = my_sympify(given, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
except Exception, err:
return {'ok': False, 'msg': 'Error %s<br/> in evaluating your expression "%s"' % (err, given)}
try:
xexpect = my_sympify(expect, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
except Exception, err:
return {'ok': False, 'msg': 'Error %s<br/> in evaluating OUR expression "%s"' % (err, expect)}
if 'autonorm' in flags: # normalize trace of matrices
try:
xgiven /= xgiven.trace()
except Exception, err:
return {'ok': False, 'msg': 'Error %s<br/> in normalizing trace of your expression %s' % (err, to_latex(xgiven))}
try:
xexpect /= xexpect.trace()
except Exception, err:
return {'ok': False, 'msg': 'Error %s<br/> in normalizing trace of OUR expression %s' % (err, to_latex(xexpect))}
msg = 'Your expression was evaluated as ' + to_latex(xgiven)
# msg += '<br/>Expected ' + to_latex(xexpect)
# msg += "<br/>flags=%s" % flags
if matrix and numerical:
xgiven = my_evalf(xgiven, chop=True)
dm = my_evalf(sympy.Matrix(xexpect) - sympy.Matrix(xgiven), chop=True)
msg += " = " + to_latex(xgiven)
if abs(dm.vec().norm().evalf()) < threshold:
return {'ok': True, 'msg': msg}
else:
pass
#msg += "dm = " + to_latex(dm) + " diff = " + str(abs(dm.vec().norm().evalf()))
#msg += "expect = " + to_latex(xexpect)
elif dosimplify:
if (sympy.simplify(xexpect) == sympy.simplify(xgiven)):
return {'ok': True, 'msg': msg}
elif numerical:
if (abs((xexpect - xgiven).evalf(chop=True)) < threshold):
return {'ok': True, 'msg': msg}
elif (xexpect == xgiven):
return {'ok': True, 'msg': msg}
#msg += "<p/>expect='%s', given='%s'" % (expect,given) # debugging
# msg += "<p/> dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y')))
return {'ok': False, 'msg': msg}
#-----------------------------------------------------------------------------
# helper function to convert all <p> to <span class='inline-error'>
def make_error_message(msg):
# msg = msg.replace('<p>','<p><span class="inline-error">').replace('</p>','</span></p>')
msg = '<div class="capa_alert">%s</div>' % msg
return msg
def is_within_tolerance(expected, actual, tolerance):
if expected == 0:
return (abs(actual) < tolerance)
else:
return (abs(abs(actual - expected) / expected) < tolerance)
#-----------------------------------------------------------------------------
# Check function interface, which takes pmathml input
#
# This is one of the main entry points to call.
def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None):
'''
Check a symbolic mathematical expression using sympy.
The input may be presentation MathML. Uses formula.
This is the default Symbolic Response checking function
Desc of args:
expect is a sympy string representing the correct answer. It is interpreted
using my_sympify (from formula.py), which reads strings as sympy input
(e.g. 'integrate(x^2, (x,1,2))' would be valid, and evaluate to give 1.5)
ans is student-typed answer. It is expected to be ascii math, but the code
below would support a sympy string.
dynamath is the PMathML string converted by MathJax. It is used if
evaluation with ans is not sufficient.
options is a string with these possible substrings, set as an xml property
of the problem:
-matrix - make a sympy matrix, rather than a list of lists, if possible
-qubit - passed to my_sympify
-imaginary - used in formla, presumably to signal to use i as sqrt(-1)?
-numerical - force numerical comparison.
'''
msg = ''
# msg += '<p/>abname=%s' % abname
# msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;'))
threshold = 1.0e-3 # for numerical comparison (also with matrices)
DEBUG = debug
if xml is not None:
DEBUG = xml.get('debug', False) # override debug flag using attribute in symbolicmath xml
if DEBUG in ['0', 'False']:
DEBUG = False
# options
do_matrix = 'matrix' in (options or '')
do_qubit = 'qubit' in (options or '')
do_imaginary = 'imaginary' in (options or '')
do_numerical = 'numerical' in (options or '')
# parse expected answer
try:
fexpect = my_sympify(str(expect), matrix=do_matrix, do_qubit=do_qubit)
except Exception, err:
msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err, expect)
return {'ok': False, 'msg': make_error_message(msg)}
###### Sympy input #######
# if expected answer is a number, try parsing provided answer as a number also
try:
fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit)
except Exception, err:
fans = None
# do a numerical comparison if both expected and answer are numbers
if (hasattr(fexpect, 'is_number') and fexpect.is_number
and hasattr(fans, 'is_number') and fans.is_number):
if is_within_tolerance(fexpect, fans, threshold):
return {'ok': True, 'msg': msg}
else:
msg += '<p>You entered: %s</p>' % to_latex(fans)
return {'ok': False, 'msg': msg}
if do_numerical: # numerical answer expected - force numerical comparison
if is_within_tolerance(fexpect, fans, threshold):
return {'ok': True, 'msg': msg}
else:
msg += '<p>You entered: %s (note that a numerical answer is expected)</p>' % to_latex(fans)
return {'ok': False, 'msg': msg}
if fexpect == fans:
msg += '<p>You entered: %s</p>' % to_latex(fans)
return {'ok': True, 'msg': msg}
###### PMathML input ######
# convert mathml answer to formula
try:
mmlans = dynamath[0] if dynamath else None
except Exception, err:
mmlans = None
if not mmlans:
return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
f = formula(mmlans, options=options)
# get sympy representation of the formula
# if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;')
try:
fsym = f.sympy
msg += '<p>You entered: %s</p>' % to_latex(f.sympy)
except Exception, err:
log.exception("Error evaluating expression '%s' as a valid equation" % ans)
msg += "<p>Error in evaluating your expression '%s' as a valid equation</p>" % (ans)
if "Illegal math" in str(err):
msg += "<p>Illegal math expression</p>"
if DEBUG:
msg += 'Error: %s' % str(err).replace('<', '&lt;')
msg += '<hr>'
msg += '<p><font color="blue">DEBUG messages:</p>'
msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
msg += '<p>cmathml=<pre>%s</pre></p>' % f.cmathml.replace('<', '&lt;')
msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<', '&lt;')
msg += '<hr>'
return {'ok': False, 'msg': make_error_message(msg)}
# do numerical comparison with expected
if hasattr(fexpect, 'is_number') and fexpect.is_number:
if hasattr(fsym, 'is_number') and fsym.is_number:
if abs(abs(fsym - fexpect) / fexpect) < threshold:
return {'ok': True, 'msg': msg}
return {'ok': False, 'msg': msg}
msg += "<p>Expecting a numerical answer!</p>"
msg += "<p>given = %s</p>" % repr(ans)
msg += "<p>fsym = %s</p>" % repr(fsym)
# msg += "<p>cmathml = <pre>%s</pre></p>" % str(f.cmathml).replace('<','&lt;')
return {'ok': False, 'msg': make_error_message(msg)}
# Here is a good spot for adding calls to X.simplify() or X.expand(),
# allowing equivalence over binomial expansion or trig identities
# exactly the same?
if fexpect == fsym:
return {'ok': True, 'msg': msg}
if type(fexpect) == list:
try:
xgiven = my_evalf(fsym, chop=True)
dm = my_evalf(sympy.Matrix(fexpect) - sympy.Matrix(xgiven), chop=True)
if abs(dm.vec().norm().evalf()) < threshold:
return {'ok': True, 'msg': msg}
except sympy.ShapeError:
msg += "<p>Error - your input vector or matrix has the wrong dimensions"
return {'ok': False, 'msg': make_error_message(msg)}
except Exception, err:
msg += "<p>Error %s in comparing expected (a list) and your answer</p>" % str(err).replace('<', '&lt;')
if DEBUG: msg += "<p/><pre>%s</pre>" % traceback.format_exc()
return {'ok': False, 'msg': make_error_message(msg)}
#diff = (fexpect-fsym).simplify()
#fsym = fsym.simplify()
#fexpect = fexpect.simplify()
try:
diff = (fexpect - fsym)
except Exception, err:
diff = None
if DEBUG:
msg += '<hr>'
msg += '<p><font color="blue">DEBUG messages:</p>'
msg += "<p>Got: %s</p>" % repr(fsym)
# msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;')
msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**', '^').replace('hat(I)', 'hat(i)')
# msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
if diff:
msg += "<p>Difference: %s</p>" % to_latex(diff)
msg += '<hr>'
# Used to return more keys: 'ex': fexpect, 'got': fsym
return {'ok': False, 'msg': msg}

13
common/lib/chem/setup.py Normal file
View File

@@ -0,0 +1,13 @@
from setuptools import setup
setup(
name="chem",
version="0.1",
packages=["chem"],
install_requires=[
"pyparsing==1.5.6",
"numpy",
"scipy",
"nltk==2.0.4",
],
)

View File

@@ -0,0 +1 @@
This directory is in the Python path for sandboxed Python execution.

View File

@@ -0,0 +1,14 @@
from setuptools import setup
setup(
name="sandbox-packages",
version="0.1",
packages=[
"verifiers",
],
py_modules=[
"eia",
],
install_requires=[
],
)

View File

@@ -13,13 +13,10 @@ real time, next to the input box.
<p>This is a correct answer which may be entered below: </p>
<p><tt>cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]</tt></p>
<script>
from symmath import *
</script>
<text>Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 &amp; 1 \\ 1 &amp; 0 \end{matrix} \right] \right) [/mathjax]
and give the resulting \(2 \times 2\) matrix. <br/>
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>. <br/>
[mathjax]U=[/mathjax] <symbolicresponse cfn="symmath_check" answer="[[cos(theta),I*sin(theta)],[I*sin(theta),cos(theta)]]" options="matrix,imaginaryi" id="filenamedogi0VpEBOWedxsymmathresponse_1" state="unsubmitted">
[mathjax]U=[/mathjax] <symbolicresponse cfn="symmath_check" answer="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]" options="matrix,imaginary" id="filenamedogi0VpEBOWedxsymmathresponse_1" state="unsubmitted">
<textline size="80" math="1" response_id="2" answer_id="1" id="filenamedogi0VpEBOWedxsymmathresponse_2_1"/>
</symbolicresponse>
<br/>

View File

@@ -3,7 +3,9 @@ import datetime
import hashlib
import json
import logging
import os
import traceback
import struct
import sys
from pkg_resources import resource_string
@@ -23,8 +25,10 @@ from xmodule.util.date_utils import time_to_datetime
log = logging.getLogger("mitx.courseware")
# Generated this many different variants of problems with rerandomize=per_student
# Generate this many different variants of problems with rerandomize=per_student
NUM_RANDOMIZATION_BINS = 20
# Never produce more than this many different seeds, no matter what.
MAX_RANDOMIZATION_BINS = 1000
def randomization_bin(seed, problem_id):
@@ -128,11 +132,7 @@ class CapaModule(CapaFields, XModule):
self.close_date = due_date
if self.seed is None:
if self.rerandomize == 'never':
self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
# see comment on randomization_bin
self.seed = randomization_bin(system.seed, self.location.url)
self.choose_new_seed()
# Need the problem location in openendedresponse to send out. Adding
# it to the system here seems like the least clunky way to get it
@@ -176,6 +176,22 @@ class CapaModule(CapaFields, XModule):
self.set_state_from_lcp()
assert self.seed is not None
def choose_new_seed(self):
"""Choose a new seed."""
if self.rerandomize == 'never':
self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
# see comment on randomization_bin
self.seed = randomization_bin(self.system.seed, self.location.url)
else:
self.seed = struct.unpack('i', os.urandom(4))[0]
# So that sandboxed code execution can be cached, but still have an interesting
# number of possibilities, cap the number of different random seeds.
self.seed %= MAX_RANDOMIZATION_BINS
def new_lcp(self, state, text=None):
if text is None:
text = self.data
@@ -184,6 +200,7 @@ class CapaModule(CapaFields, XModule):
problem_text=text,
id=self.location.html_id(),
state=state,
seed=self.seed,
system=self.system,
)
@@ -851,14 +868,11 @@ class CapaModule(CapaFields, XModule):
'error': "Refresh the page and make an attempt before resetting."}
if self.rerandomize in ["always", "onreset"]:
# reset random number generator seed (note the self.lcp.get_state()
# in next line)
seed = None
else:
seed = self.lcp.seed
# Reset random number generator seed.
self.choose_new_seed()
# Generate a new problem with either the previous seed or a new seed
self.lcp = self.new_lcp({'seed': seed})
self.lcp = self.new_lcp(None)
# Pull in the new problem seed
self.set_state_from_lcp()

View File

@@ -31,11 +31,11 @@ class ModuleStoreTestCase(TestCase):
@staticmethod
def load_templates_if_necessary():
'''
Load templates into the modulestore only if they do not already exist.
Load templates into the direct modulestore only if they do not already exist.
We need the templates, because they are copied to create
XModules such as sections and problems
'''
modulestore = xmodule.modulestore.django.modulestore()
modulestore = xmodule.modulestore.django.modulestore('direct')
# Count the number of templates
query = {"_id.course": "templates"}

View File

@@ -14,7 +14,7 @@ import fs.osfs
import numpy
import capa.calc as calc
import calc
import xmodule
from xmodule.x_module import ModuleSystem
from mock import Mock
@@ -33,15 +33,14 @@ def test_system():
"""
Construct a test ModuleSystem instance.
By default, the render_template() method simply returns
the context it is passed as a string.
You can override this behavior by monkey patching:
By default, the render_template() method simply returns the context it is
passed as a string. You can override this behavior by monkey patching::
system = test_system()
system.render_template = my_render_func
system = test_system()
system.render_template = my_render_func
where `my_render_func` is a function of the form my_render_func(template, context).
where my_render_func is a function of the form
my_render_func(template, context)
"""
return ModuleSystem(
ajax_url='courses/course_id/modx/a_location',
@@ -86,10 +85,12 @@ class ModelsTest(unittest.TestCase):
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)") + 1) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1") - 0.5 - 0.5j) < 0.00001)
variables['t'] = 1.0
# Use self.assertAlmostEqual here...
self.assertTrue(abs(calc.evaluator(variables, functions, "t") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True) - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True) - 298) < 0.2)
# Use self.assertRaises here...
exception_happened = False
try:
calc.evaluator({}, {}, "5+7 QWSEKO")

View File

@@ -550,6 +550,7 @@ class CapaModuleTest(unittest.TestCase):
def test_reset_problem(self):
module = CapaFactory.create(done=True)
module.new_lcp = Mock(wraps=module.new_lcp)
module.choose_new_seed = Mock(wraps=module.choose_new_seed)
# Stub out HTML rendering
with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
@@ -567,7 +568,8 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(result['html'], "<div>Test HTML</div>")
# Expect that the problem was reset
module.new_lcp.assert_called_once_with({'seed': None})
module.new_lcp.assert_called_once_with(None)
module.choose_new_seed.assert_called_once_with()
def test_reset_problem_closed(self):
module = CapaFactory.create()
@@ -1033,3 +1035,13 @@ class CapaModuleTest(unittest.TestCase):
self.assertTrue(module.seed is not None)
msg = 'Could not get a new seed from reset after 5 tries'
self.assertTrue(success, msg)
def test_random_seed_bins(self):
# Assert that we are limiting the number of possible seeds.
# Check the conditions that generate random seeds
for rerandomize in ['always', 'per_student', 'true', 'onreset']:
# Get a bunch of seeds, they should all be in 0-999.
for i in range(200):
module = CapaFactory.create(rerandomize=rerandomize)
assert 0 <= module.seed < 1000

View File

@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase):
'''
def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(test_system, 'a://b/c/d/e', None, {})
xm = x_module.XModule(test_system(), 'a://b/c/d/e', None, {})
p = xm.get_progress()
self.assertEqual(p, None)

View File

@@ -14,7 +14,6 @@ START = '2013-01-01T01:00:00'
from .test_course_module import DummySystem as DummyImportSystem
from . import test_system
class RandomizeModuleTestCase(unittest.TestCase):

View File

@@ -763,7 +763,10 @@ class ModuleSystem(object):
anonymous_student_id='',
course_id=None,
open_ended_grading_interface=None,
s3_interface=None):
s3_interface=None,
cache=None,
can_execute_unsafe_code=None,
):
'''
Create a closure around the system environment.
@@ -805,6 +808,14 @@ class ModuleSystem(object):
xblock_model_data - A dict-like object containing the all data available to this
xblock
cache - A cache object with two methods:
.get(key) returns an object from the cache or None.
.set(key, value, timeout_secs=None) stores a value in the cache with a timeout.
can_execute_unsafe_code - A function returning a boolean, whether or
not to allow the execution of unsafe, unsandboxed code.
'''
self.ajax_url = ajax_url
self.xqueue = xqueue
@@ -829,6 +840,9 @@ class ModuleSystem(object):
self.open_ended_grading_interface = open_ended_grading_interface
self.s3_interface = s3_interface
self.cache = cache or DoNothingCache()
self.can_execute_unsafe_code = can_execute_unsafe_code or (lambda: False)
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
return self.__dict__.get(attr)
@@ -842,3 +856,12 @@ class ModuleSystem(object):
def __str__(self):
return str(self.__dict__)
class DoNothingCache(object):
"""A duck-compatible object to use in ModuleSystem when there's no cache."""
def get(self, key):
return None
def set(self, key, value, timeout=None):
pass

View File

@@ -0,0 +1,66 @@
describe 'All Content', ->
beforeEach ->
# TODO: figure out a better way of handling this
# It is set up in main.coffee DiscussionApp.start
window.$$course_id = 'mitX/999/test'
window.user = new DiscussionUser {id: '567'}
describe 'Content', ->
beforeEach ->
@content = new Content {
id: '01234567',
user_id: '567',
course_id: 'mitX/999/test',
body: 'this is some content',
abuse_flaggers: ['123']
}
it 'should exist', ->
expect(Content).toBeDefined()
it 'is initialized correctly', ->
@content.initialize
expect(Content.contents['01234567']).toEqual @content
expect(@content.get 'id').toEqual '01234567'
expect(@content.get 'user_url').toEqual '/courses/mitX/999/test/discussion/forum/users/567'
expect(@content.get 'children').toEqual []
expect(@content.get 'comments').toEqual(jasmine.any(Comments))
it 'can update info', ->
@content.updateInfo {
ability: 'can_endorse',
voted: true,
subscribed: true
}
expect(@content.get 'ability').toEqual 'can_endorse'
expect(@content.get 'voted').toEqual true
expect(@content.get 'subscribed').toEqual true
it 'can be flagged for abuse', ->
@content.flagAbuse()
expect(@content.get 'abuse_flaggers').toEqual ['123', '567']
it 'can be unflagged for abuse', ->
temp_array = []
temp_array.push(window.user.get('id'))
@content.set("abuse_flaggers",temp_array)
@content.unflagAbuse()
expect(@content.get 'abuse_flaggers').toEqual []
describe 'Comments', ->
beforeEach ->
@comment1 = new Comment {id: '123'}
@comment2 = new Comment {id: '345'}
it 'can contain multiple comments', ->
myComments = new Comments
expect(myComments.length).toEqual 0
myComments.add @comment1
expect(myComments.length).toEqual 1
myComments.add @comment2
expect(myComments.length).toEqual 2
it 'returns results to the find method', ->
myComments = new Comments
myComments.add @comment1
expect(myComments.find('123')).toBe @comment1

View File

@@ -0,0 +1,58 @@
describe "DiscussionContentView", ->
beforeEach ->
setFixtures
(
"""
<div class="discussion-post">
<header>
<a data-tooltip="vote" data-role="discussion-vote" class="vote-btn discussion-vote discussion-vote-up" href="#">
<span class="plus-icon">+</span> <span class="votes-count-number">0</span></a>
<h1>Post Title</h1>
<p class="posted-details">
<a class="username" href="/courses/MITx/999/Robot_Super_Course/discussion/forum/users/1">robot</a>
<span title="2013-05-08T17:34:07Z" class="timeago">less than a minute ago</span>
</p>
</header>
<div class="post-body"><p>Post body.</p></div>
<div data-tooltip="Report Misuse" data-role="thread-flag" class="discussion-flag-abuse notflagged">
<i class="icon"></i><span class="flag-label">Report Misuse</span></div>
<div data-tooltip="pin this thread" data-role="thread-pin" class="admin-pin discussion-pin notpinned">
<i class="icon"></i><span class="pin-label">Pin Thread</span></div>
</div>
"""
)
@thread = new Thread {
id: '01234567',
user_id: '567',
course_id: 'mitX/999/test',
body: 'this is a thread',
created_at: '2013-04-03T20:08:39Z',
abuse_flaggers: ['123']
roles: []
}
@view = new DiscussionContentView({ model: @thread })
it 'defines the tag', ->
expect($('#jasmine-fixtures')).toExist
expect(@view.tagName).toBeDefined
expect(@view.el.tagName.toLowerCase()).toBe 'div'
it "defines the class", ->
# spyOn @content, 'initialize'
expect(@view.model).toBeDefined();
it 'is tied to the model', ->
expect(@view.model).toBeDefined();
it 'can be flagged for abuse', ->
@thread.flagAbuse()
expect(@thread.get 'abuse_flaggers').toEqual ['123', '567']
it 'can be unflagged for abuse', ->
temp_array = []
temp_array.push(window.user.get('id'))
@thread.set("abuse_flaggers",temp_array)
@thread.unflagAbuse()
expect(@thread.get 'abuse_flaggers').toEqual []

View File

@@ -0,0 +1,62 @@
describe 'ResponseCommentShowView', ->
beforeEach ->
# set up the container for the response to go in
setFixtures """
<ol class="responses"></ol>
<script id="response-comment-show-template" type="text/template">
<div id="comment_<%- id %>">
<div class="response-body"><%- body %></div>
<div class="discussion-flag-abuse notflagged" data-role="thread-flag" data-tooltip="report misuse">
<i class="icon"></i><span class="flag-label"></span></div>
<p class="posted-details">&ndash;posted <span class="timeago" title="<%- created_at %>"><%- created_at %></span> by
<% if (obj.username) { %>
<a href="<%- user_url %>" class="profile-link"><%- username %></a>
<% } else {print('anonymous');} %>
</p>
</div>
</script>
"""
# set up a model for a new Comment
@response = new Comment {
id: '01234567',
user_id: '567',
course_id: 'mitX/999/test',
body: 'this is a response',
created_at: '2013-04-03T20:08:39Z',
abuse_flaggers: ['123']
roles: []
}
@view = new ResponseCommentShowView({ model: @response })
# spyOn(DiscussionUtil, 'loadRoles').andReturn []
it 'defines the tag', ->
expect($('#jasmine-fixtures')).toExist
expect(@view.tagName).toBeDefined
expect(@view.el.tagName.toLowerCase()).toBe 'li'
it 'is tied to the model', ->
expect(@view.model).toBeDefined();
describe 'rendering', ->
beforeEach ->
spyOn(@view, 'renderAttrs')
spyOn(@view, 'markAsStaff')
spyOn(@view, 'convertMath')
it 'produces the correct HTML', ->
@view.render()
expect(@view.el.innerHTML).toContain('"discussion-flag-abuse notflagged"')
it 'can be flagged for abuse', ->
@response.flagAbuse()
expect(@response.get 'abuse_flaggers').toEqual ['123', '567']
it 'can be unflagged for abuse', ->
temp_array = []
temp_array.push(window.user.get('id'))
@response.set("abuse_flaggers",temp_array)
@response.unflagAbuse()
expect(@response.get 'abuse_flaggers').toEqual []

View File

@@ -1,6 +1,5 @@
describe 'Logger', ->
it 'expose window.log_event', ->
jasmine.stubRequests()
expect(window.log_event).toBe Logger.log
describe 'log', ->
@@ -12,7 +11,8 @@ describe 'Logger', ->
event: '"data"'
page: window.location.href
describe 'bind', ->
# Broken with commit 9f75e64? Skipping for now.
xdescribe 'bind', ->
beforeEach ->
Logger.bind()
Courseware.prefix = '/6002x'

View File

@@ -88,20 +88,32 @@ if Backbone?
pinned = @get("pinned")
@set("pinned",pinned)
@trigger "change", @
flagAbuse: ->
temp_array = @get("abuse_flaggers")
temp_array.push(window.user.get('id'))
@set("abuse_flaggers",temp_array)
@trigger "change", @
unflagAbuse: ->
@get("abuse_flaggers").pop(window.user.get('id'))
@trigger "change", @
class @Thread extends @Content
urlMappers:
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
@@ -157,6 +169,8 @@ if Backbone?
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
'update': -> DiscussionUtil.urlFor('update_comment', @id)
'delete': -> DiscussionUtil.urlFor('delete_comment', @id)
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
getCommentsCount: ->
count = 0

View File

@@ -37,6 +37,9 @@ if Backbone?
data['commentable_ids'] = options.commentable_ids
when 'all'
url = DiscussionUtil.urlFor 'threads'
when 'flagged'
data['flagged'] = true
url = DiscussionUtil.urlFor 'search'
when 'followed'
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
if options['group_id']

View File

@@ -18,8 +18,12 @@ class @DiscussionUtil
@loadRoles: (roles)->
@roleIds = roles
@loadFlagModerator: (what)->
@isFlagModerator = ((what=="True") or (what == 1))
@loadRolesFromContainer: ->
@loadRoles($("#discussion-container").data("roles"))
@loadFlagModerator($("#discussion-container").data("flag-moderator"))
@isStaff: (user_id) ->
staff = _.union(@roleIds['Staff'], @roleIds['Moderator'], @roleIds['Administrator'])
@@ -48,9 +52,13 @@ class @DiscussionUtil
update_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/update"
create_comment : "/courses/#{$$course_id}/discussion/threads/#{param}/reply"
delete_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/delete"
flagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/flagAbuse"
unFlagAbuse_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unFlagAbuse"
flagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/flagAbuse"
unFlagAbuse_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/unFlagAbuse"
upvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/upvote"
downvote_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/downvote"
pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/pin"
un_pin_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unpin"
undo_vote_for_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/unvote"
follow_thread : "/courses/#{$$course_id}/discussion/threads/#{param}/follow"
@@ -72,7 +80,7 @@ class @DiscussionUtil
permanent_link_thread : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}"
permanent_link_comment : "/courses/#{$$course_id}/discussion/forum/#{param}/threads/#{param1}##{param2}"
user_profile : "/courses/#{$$course_id}/discussion/forum/users/#{param}"
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
followed_threads : "/courses/#{$$course_id}/discussion/forum/users/#{param}/followed"
threads : "/courses/#{$$course_id}/discussion/forum"
}[name]

View File

@@ -1,6 +1,11 @@
if Backbone?
class @DiscussionContentView extends Backbone.View
events:
"click .discussion-flag-abuse": "toggleFlagAbuse"
attrRenderer:
endorsed: (endorsed) ->
if endorsed
@@ -94,7 +99,48 @@ if Backbone?
setWmdContent: (cls_identifier, text) =>
DiscussionUtil.setWmdContent @$el, $.proxy(@$, @), cls_identifier, text
initialize: ->
@initLocal()
@model.bind('change', @renderPartialAttrs, @)
toggleFlagAbuse: (event) ->
event.preventDefault()
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@unFlagAbuse()
else
@flagAbuse()
flagAbuse: ->
url = @model.urlFor("flagAbuse")
DiscussionUtil.safeAjax
$elem: @$(".discussion-flag-abuse")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
###
note, we have to clone the array in order to trigger a change event
###
temp_array = _.clone(@model.get('abuse_flaggers'));
temp_array.push(window.user.id)
@model.set('abuse_flaggers', temp_array)
unFlagAbuse: ->
url = @model.urlFor("unFlagAbuse")
DiscussionUtil.safeAjax
$elem: @$(".discussion-flag-abuse")
url: url
type: "POST"
success: (response, textStatus) =>
if textStatus == 'success'
temp_array = _.clone(@model.get('abuse_flaggers'));
temp_array.pop(window.user.id)
# if you're an admin, clear this
if DiscussionUtil.isFlagModerator
temp_array = []
@model.set('abuse_flaggers', temp_array)

View File

@@ -276,6 +276,11 @@ if Backbone?
@$(".post-search-field").val("")
@$('.cohort').show()
@retrieveAllThreads()
else if discussionId == "#flagged"
@discussionIds = ""
@$(".post-search-field").val("")
@$('.cohort').hide()
@retrieveFlaggedThreads()
else if discussionId == "#following"
@retrieveFollowed(event)
@$('.cohort').hide()
@@ -321,6 +326,12 @@ if Backbone?
@collection.reset()
@loadMorePages(event)
retrieveFlaggedThreads: (event)->
@collection.current_page = 0
@collection.reset()
@mode = 'flagged'
@loadMorePages(event)
sortThreads: (event) ->
@$(".sort-bar a").removeClass("active")
$(event.target).addClass("active")

View File

@@ -3,6 +3,7 @@ if Backbone?
events:
"click .discussion-vote": "toggleVote"
"click .discussion-flag-abuse": "toggleFlagAbuse"
"click .admin-pin": "togglePin"
"click .action-follow": "toggleFollowing"
"click .action-edit": "edit"
@@ -25,6 +26,7 @@ if Backbone?
@delegateEvents()
@renderDogear()
@renderVoted()
@renderFlagged()
@renderPinned()
@renderAttrs()
@$("span.timeago").timeago()
@@ -42,6 +44,16 @@ if Backbone?
@$("[data-role=discussion-vote]").addClass("is-cast")
else
@$("[data-role=discussion-vote]").removeClass("is-cast")
renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged")
@$("[data-role=thread-flag]").removeClass("notflagged")
@$(".discussion-flag-abuse .flag-label").html("Misuse Reported")
else
@$("[data-role=thread-flag]").removeClass("flagged")
@$("[data-role=thread-flag]").addClass("notflagged")
@$(".discussion-flag-abuse .flag-label").html("Report Misuse")
renderPinned: =>
if @model.get("pinned")
@@ -56,6 +68,7 @@ if Backbone?
updateModelDetails: =>
@renderVoted()
@renderFlagged()
@renderPinned()
@$("[data-role=discussion-vote] .votes-count-number").html(@model.get("votes")["up_count"])
@@ -96,6 +109,7 @@ if Backbone?
if textStatus == 'success'
@model.set(response, {silent: true})
unvote: ->
window.user.unvote(@model)
url = @model.urlFor("unvote")
@@ -107,6 +121,7 @@ if Backbone?
if textStatus == 'success'
@model.set(response, {silent: true})
edit: (event) ->
@trigger "thread:edit", event
@@ -182,4 +197,4 @@ if Backbone?
params = $.extend(params, user:{username: @model.username, user_url: @model.user_url})
Mustache.render(@template, params)

View File

@@ -91,7 +91,7 @@ if Backbone?
body = @getWmdContent("reply-body")
return if not body.trim().length
@setWmdContent("reply-body", "")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, endorsed: false, user_id: window.user.get("id"))
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), votes: { up_count: 0 }, abuse_flaggers:[], endorsed: false, user_id: window.user.get("id"))
comment.set('thread', @model.get('thread'))
@renderResponse(comment)
@model.addComment()

View File

@@ -1,8 +1,15 @@
if Backbone?
class @ResponseCommentShowView extends DiscussionContentView
events:
"click .discussion-flag-abuse": "toggleFlagAbuse"
tagName: "li"
initialize: ->
super()
@model.on "change", @updateModelDetails
render: ->
@template = _.template($("#response-comment-show-template").html())
params = @model.toJSON()
@@ -11,6 +18,7 @@ if Backbone?
@initLocal()
@delegateEvents()
@renderAttrs()
@renderFlagged()
@markAsStaff()
@$el.find(".timeago").timeago()
@convertMath()
@@ -34,3 +42,17 @@ if Backbone?
@$el.find("a.profile-link").after('<span class="staff-label">staff</span>')
else if DiscussionUtil.isTA(@model.get("user_id"))
@$el.find("a.profile-link").after('<span class="community-ta-label">Community&nbsp;&nbsp;TA</span>')
renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged")
@$("[data-role=thread-flag]").removeClass("notflagged")
else
@$("[data-role=thread-flag]").removeClass("flagged")
@$("[data-role=thread-flag]").addClass("notflagged")
updateModelDetails: =>
@renderFlagged()

View File

@@ -5,6 +5,7 @@ if Backbone?
"click .action-endorse": "toggleEndorse"
"click .action-delete": "delete"
"click .action-edit": "edit"
"click .discussion-flag-abuse": "toggleFlagAbuse"
$: (selector) ->
@$el.find(selector)
@@ -23,6 +24,7 @@ if Backbone?
if window.user.voted(@model)
@$(".vote-btn").addClass("is-cast")
@renderAttrs()
@renderFlagged()
@$el.find(".posted-details").timeago()
@convertMath()
@markAsStaff()
@@ -70,6 +72,7 @@ if Backbone?
success: (response, textStatus) =>
if textStatus == 'success'
@model.set(response)
edit: (event) ->
@trigger "response:edit", event
@@ -92,3 +95,17 @@ if Backbone?
url: url
data: data
type: "POST"
renderFlagged: =>
if window.user.id in @model.get("abuse_flaggers") or (DiscussionUtil.isFlagModerator and @model.get("abuse_flaggers").length > 0)
@$("[data-role=thread-flag]").addClass("flagged")
@$("[data-role=thread-flag]").removeClass("notflagged")
@$(".discussion-flag-abuse .flag-label").html("Misuse Reported")
else
@$("[data-role=thread-flag]").removeClass("flagged")
@$("[data-role=thread-flag]").addClass("notflagged")
@$(".discussion-flag-abuse .flag-label").html("Report Misuse")
updateModelDetails: =>
@renderFlagged()

View File

@@ -77,7 +77,7 @@ if Backbone?
body = @getWmdContent("comment-body")
return if not body.trim().length
@setWmdContent("comment-body", "")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), user_id: window.user.get("id"), id:"unsaved")
comment = new Comment(body: body, created_at: (new Date()).toISOString(), username: window.user.get("username"), abuse_flaggers:[], user_id: window.user.get("id"), id:"unsaved")
view = @renderComment(comment)
@hideEditorChrome()
@trigger "comment:add", comment

1827
common/static/js/vendor/annotator.js vendored Normal file
View File

@@ -0,0 +1,1827 @@
/*
** Annotator 1.2.6-dev-dc18206
** https://github.com/okfn/annotator/
**
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2013-05-16 18:01:57Z
*/
(function() {
var $, Annotator, Delegator, LinkParser, Range, findChild, fn, functions, g, getNodeName, getNodePosition, gettext, simpleXPathJQuery, simpleXPathPure, util, _Annotator, _gettext, _i, _j, _len, _len1, _ref, _ref1, _t,
__slice = [].slice,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
simpleXPathJQuery = function(relativeRoot) {
var jq;
jq = this.map(function() {
var elem, idx, path, tagName;
path = '';
elem = this;
while (elem && elem.nodeType === 1 && elem !== relativeRoot) {
tagName = elem.tagName.replace(":", "\\:");
idx = $(elem.parentNode).children(tagName).index(elem) + 1;
idx = "[" + idx + "]";
path = "/" + elem.tagName.toLowerCase() + idx + path;
elem = elem.parentNode;
}
return path;
});
return jq.get();
};
simpleXPathPure = function(relativeRoot) {
var getPathSegment, getPathTo, jq, rootNode;
getPathSegment = function(node) {
var name, pos;
name = getNodeName(node);
pos = getNodePosition(node);
return "" + name + "[" + pos + "]";
};
rootNode = relativeRoot;
getPathTo = function(node) {
var xpath;
xpath = '';
while (node !== rootNode) {
if (node == null) {
throw new Error("Called getPathTo on a node which was not a descendant of @rootNode. " + rootNode);
}
xpath = (getPathSegment(node)) + '/' + xpath;
node = node.parentNode;
}
xpath = '/' + xpath;
xpath = xpath.replace(/\/$/, '');
return xpath;
};
jq = this.map(function() {
var path;
path = getPathTo(this);
return path;
});
return jq.get();
};
findChild = function(node, type, index) {
var child, children, found, name, _i, _len;
if (!node.hasChildNodes()) {
throw new Error("XPath error: node has no children!");
}
children = node.childNodes;
found = 0;
for (_i = 0, _len = children.length; _i < _len; _i++) {
child = children[_i];
name = getNodeName(child);
if (name === type) {
found += 1;
if (found === index) {
return child;
}
}
}
throw new Error("XPath error: wanted child not found.");
};
getNodeName = function(node) {
var nodeName;
nodeName = node.nodeName.toLowerCase();
switch (nodeName) {
case "#text":
return "text()";
case "#comment":
return "comment()";
case "#cdata-section":
return "cdata-section()";
default:
return nodeName;
}
};
getNodePosition = function(node) {
var pos, tmp;
pos = 0;
tmp = node;
while (tmp) {
if (tmp.nodeName === node.nodeName) {
pos++;
}
tmp = tmp.previousSibling;
}
return pos;
};
gettext = null;
if (typeof Gettext !== "undefined" && Gettext !== null) {
_gettext = new Gettext({
domain: "annotator"
});
gettext = function(msgid) {
return _gettext.gettext(msgid);
};
} else {
gettext = function(msgid) {
return msgid;
};
}
_t = function(msgid) {
return gettext(msgid);
};
if (!(typeof jQuery !== "undefined" && jQuery !== null ? (_ref = jQuery.fn) != null ? _ref.jquery : void 0 : void 0)) {
console.error(_t("Annotator requires jQuery: have you included lib/vendor/jquery.js?"));
}
if (!(JSON && JSON.parse && JSON.stringify)) {
console.error(_t("Annotator requires a JSON implementation: have you included lib/vendor/json2.js?"));
}
$ = jQuery.sub();
$.flatten = function(array) {
var flatten;
flatten = function(ary) {
var el, flat, _i, _len;
flat = [];
for (_i = 0, _len = ary.length; _i < _len; _i++) {
el = ary[_i];
flat = flat.concat(el && $.isArray(el) ? flatten(el) : el);
}
return flat;
};
return flatten(array);
};
$.plugin = function(name, object) {
return jQuery.fn[name] = function(options) {
var args;
args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var instance;
instance = $.data(this, name);
if (instance) {
return options && instance[options].apply(instance, args);
} else {
instance = new object(this, options);
return $.data(this, name, instance);
}
});
};
};
$.fn.textNodes = function() {
var getTextNodes;
getTextNodes = function(node) {
var nodes;
if (node && node.nodeType !== 3) {
nodes = [];
if (node.nodeType !== 8) {
node = node.lastChild;
while (node) {
nodes.push(getTextNodes(node));
node = node.previousSibling;
}
}
return nodes.reverse();
} else {
return node;
}
};
return this.map(function() {
return $.flatten(getTextNodes(this));
});
};
$.fn.xpath = function(relativeRoot) {
var exception, result;
try {
result = simpleXPathJQuery.call(this, relativeRoot);
} catch (_error) {
exception = _error;
console.log("jQuery-based XPath construction failed! Falling back to manual.");
result = simpleXPathPure.call(this, relativeRoot);
}
return result;
};
$.xpath = function(xp, root) {
var idx, name, node, step, steps, _i, _len, _ref1;
steps = xp.substring(1).split("/");
node = root;
for (_i = 0, _len = steps.length; _i < _len; _i++) {
step = steps[_i];
_ref1 = step.split("["), name = _ref1[0], idx = _ref1[1];
idx = idx != null ? parseInt((idx != null ? idx.split("]") : void 0)[0]) : 1;
node = findChild(node, name.toLowerCase(), idx);
}
return node;
};
$.escape = function(html) {
return html.replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
};
$.fn.escape = function(html) {
if (arguments.length) {
return this.html($.escape(html));
}
return this.html();
};
$.fn.reverse = []._reverse || [].reverse;
functions = ["log", "debug", "info", "warn", "exception", "assert", "dir", "dirxml", "trace", "group", "groupEnd", "groupCollapsed", "time", "timeEnd", "profile", "profileEnd", "count", "clear", "table", "error", "notifyFirebug", "firebug", "userObjects"];
if (typeof console !== "undefined" && console !== null) {
if (console.group == null) {
console.group = function(name) {
return console.log("GROUP: ", name);
};
}
if (console.groupCollapsed == null) {
console.groupCollapsed = console.group;
}
for (_i = 0, _len = functions.length; _i < _len; _i++) {
fn = functions[_i];
if (console[fn] == null) {
console[fn] = function() {
return console.log(_t("Not implemented:") + (" console." + name));
};
}
}
} else {
this.console = {};
for (_j = 0, _len1 = functions.length; _j < _len1; _j++) {
fn = functions[_j];
this.console[fn] = function() {};
}
this.console['error'] = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return alert("ERROR: " + (args.join(', ')));
};
this.console['warn'] = function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return alert("WARNING: " + (args.join(', ')));
};
}
Delegator = (function() {
Delegator.prototype.events = {};
Delegator.prototype.options = {};
Delegator.prototype.element = null;
function Delegator(element, options) {
this.options = $.extend(true, {}, this.options, options);
this.element = $(element);
this.on = this.subscribe;
this.addEvents();
}
Delegator.prototype.addEvents = function() {
var event, functionName, sel, selector, _k, _ref1, _ref2, _results;
_ref1 = this.events;
_results = [];
for (sel in _ref1) {
functionName = _ref1[sel];
_ref2 = sel.split(' '), selector = 2 <= _ref2.length ? __slice.call(_ref2, 0, _k = _ref2.length - 1) : (_k = 0, []), event = _ref2[_k++];
_results.push(this.addEvent(selector.join(' '), event, functionName));
}
return _results;
};
Delegator.prototype.addEvent = function(bindTo, event, functionName) {
var closure, isBlankSelector,
_this = this;
closure = function() {
return _this[functionName].apply(_this, arguments);
};
isBlankSelector = typeof bindTo === 'string' && bindTo.replace(/\s+/g, '') === '';
if (isBlankSelector) {
bindTo = this.element;
}
if (typeof bindTo === 'string') {
this.element.delegate(bindTo, event, closure);
} else {
if (this.isCustomEvent(event)) {
this.subscribe(event, closure);
} else {
$(bindTo).bind(event, closure);
}
}
return this;
};
Delegator.prototype.isCustomEvent = function(event) {
event = event.split('.')[0];
return $.inArray(event, Delegator.natives) === -1;
};
Delegator.prototype.publish = function() {
this.element.triggerHandler.apply(this.element, arguments);
return this;
};
Delegator.prototype.subscribe = function(event, callback) {
var closure;
closure = function() {
return callback.apply(this, [].slice.call(arguments, 1));
};
closure.guid = callback.guid = ($.guid += 1);
this.element.bind(event, closure);
return this;
};
Delegator.prototype.unsubscribe = function() {
this.element.unbind.apply(this.element, arguments);
return this;
};
return Delegator;
})();
Delegator.natives = (function() {
var key, specials, val;
specials = (function() {
var _ref1, _results;
_ref1 = jQuery.event.special;
_results = [];
for (key in _ref1) {
if (!__hasProp.call(_ref1, key)) continue;
val = _ref1[key];
_results.push(key);
}
return _results;
})();
return "blur focus focusin focusout load resize scroll unload click dblclick\nmousedown mouseup mousemove mouseover mouseout mouseenter mouseleave\nchange select submit keydown keypress keyup error".split(/[^a-z]+/).concat(specials);
})();
Range = {};
Range.sniff = function(r) {
if (r.commonAncestorContainer != null) {
return new Range.BrowserRange(r);
} else if (typeof r.start === "string") {
return new Range.SerializedRange(r);
} else if (r.start && typeof r.start === "object") {
return new Range.NormalizedRange(r);
} else {
console.error(_t("Could not sniff range type"));
return false;
}
};
Range.nodeFromXPath = function(xpath, root) {
var customResolver, evaluateXPath, namespace, node, segment;
if (root == null) {
root = document;
}
evaluateXPath = function(xp, nsResolver) {
var exception;
if (nsResolver == null) {
nsResolver = null;
}
try {
return document.evaluate('.' + xp, root, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
} catch (_error) {
exception = _error;
console.log("XPath evaluation failed.");
console.log("Trying fallback...");
return $.xpath(xp, root);
}
};
if (!$.isXMLDoc(document.documentElement)) {
return evaluateXPath(xpath);
} else {
customResolver = document.createNSResolver(document.ownerDocument === null ? document.documentElement : document.ownerDocument.documentElement);
node = evaluateXPath(xpath, customResolver);
if (!node) {
xpath = ((function() {
var _k, _len2, _ref1, _results;
_ref1 = xpath.split('/');
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
segment = _ref1[_k];
if (segment && segment.indexOf(':') === -1) {
_results.push(segment.replace(/^([a-z]+)/, 'xhtml:$1'));
} else {
_results.push(segment);
}
}
return _results;
})()).join('/');
namespace = document.lookupNamespaceURI(null);
customResolver = function(ns) {
if (ns === 'xhtml') {
return namespace;
} else {
return document.documentElement.getAttribute('xmlns:' + ns);
}
};
node = evaluateXPath(xpath, customResolver);
}
return node;
}
};
Range.RangeError = (function(_super) {
__extends(RangeError, _super);
function RangeError(type, message, parent) {
this.type = type;
this.message = message;
this.parent = parent != null ? parent : null;
RangeError.__super__.constructor.call(this, this.message);
}
return RangeError;
})(Error);
Range.BrowserRange = (function() {
function BrowserRange(obj) {
this.commonAncestorContainer = obj.commonAncestorContainer;
this.startContainer = obj.startContainer;
this.startOffset = obj.startOffset;
this.endContainer = obj.endContainer;
this.endOffset = obj.endOffset;
}
BrowserRange.prototype.normalize = function(root) {
var it, node, nr, offset, p, r, _k, _len2, _ref1;
if (this.tainted) {
console.error(_t("You may only call normalize() once on a BrowserRange!"));
return false;
} else {
this.tainted = true;
}
r = {};
nr = {};
_ref1 = ['start', 'end'];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
p = _ref1[_k];
node = this[p + 'Container'];
offset = this[p + 'Offset'];
if (node.nodeType === 1) {
it = node.childNodes[offset];
node = it || node.childNodes[offset - 1];
if (node.nodeType === 1 && !node.firstChild) {
it = null;
node = node.previousSibling;
}
while (node.nodeType !== 3) {
node = node.firstChild;
}
offset = it ? 0 : node.nodeValue.length;
}
r[p] = node;
r[p + 'Offset'] = offset;
}
nr.start = r.startOffset > 0 ? r.start.splitText(r.startOffset) : r.start;
if (r.start === r.end) {
if ((r.endOffset - r.startOffset) < nr.start.nodeValue.length) {
nr.start.splitText(r.endOffset - r.startOffset);
}
nr.end = nr.start;
} else {
if (r.endOffset < r.end.nodeValue.length) {
r.end.splitText(r.endOffset);
}
nr.end = r.end;
}
nr.commonAncestor = this.commonAncestorContainer;
while (nr.commonAncestor.nodeType !== 1) {
nr.commonAncestor = nr.commonAncestor.parentNode;
}
return new Range.NormalizedRange(nr);
};
BrowserRange.prototype.serialize = function(root, ignoreSelector) {
return this.normalize(root).serialize(root, ignoreSelector);
};
return BrowserRange;
})();
Range.NormalizedRange = (function() {
function NormalizedRange(obj) {
this.commonAncestor = obj.commonAncestor;
this.start = obj.start;
this.end = obj.end;
}
NormalizedRange.prototype.normalize = function(root) {
return this;
};
NormalizedRange.prototype.limit = function(bounds) {
var nodes, parent, startParents, _k, _len2, _ref1;
nodes = $.grep(this.textNodes(), function(node) {
return node.parentNode === bounds || $.contains(bounds, node.parentNode);
});
if (!nodes.length) {
return null;
}
this.start = nodes[0];
this.end = nodes[nodes.length - 1];
startParents = $(this.start).parents();
_ref1 = $(this.end).parents();
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
parent = _ref1[_k];
if (startParents.index(parent) !== -1) {
this.commonAncestor = parent;
break;
}
}
return this;
};
NormalizedRange.prototype.serialize = function(root, ignoreSelector) {
var end, serialization, start;
serialization = function(node, isEnd) {
var n, nodes, offset, origParent, textNodes, xpath, _k, _len2;
if (ignoreSelector) {
origParent = $(node).parents(":not(" + ignoreSelector + ")").eq(0);
} else {
origParent = $(node).parent();
}
xpath = origParent.xpath(root)[0];
textNodes = origParent.textNodes();
nodes = textNodes.slice(0, textNodes.index(node));
offset = 0;
for (_k = 0, _len2 = nodes.length; _k < _len2; _k++) {
n = nodes[_k];
offset += n.nodeValue.length;
}
if (isEnd) {
return [xpath, offset + node.nodeValue.length];
} else {
return [xpath, offset];
}
};
start = serialization(this.start);
end = serialization(this.end, true);
return new Range.SerializedRange({
start: start[0],
end: end[0],
startOffset: start[1],
endOffset: end[1]
});
};
NormalizedRange.prototype.text = function() {
var node;
return ((function() {
var _k, _len2, _ref1, _results;
_ref1 = this.textNodes();
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
node = _ref1[_k];
_results.push(node.nodeValue);
}
return _results;
}).call(this)).join('');
};
NormalizedRange.prototype.textNodes = function() {
var end, start, textNodes, _ref1;
textNodes = $(this.commonAncestor).textNodes();
_ref1 = [textNodes.index(this.start), textNodes.index(this.end)], start = _ref1[0], end = _ref1[1];
return $.makeArray(textNodes.slice(start, +end + 1 || 9e9));
};
NormalizedRange.prototype.toRange = function() {
var range;
range = document.createRange();
range.setStartBefore(this.start);
range.setEndAfter(this.end);
return range;
};
return NormalizedRange;
})();
Range.SerializedRange = (function() {
function SerializedRange(obj) {
this.start = obj.start;
this.startOffset = obj.startOffset;
this.end = obj.end;
this.endOffset = obj.endOffset;
}
SerializedRange.prototype.normalize = function(root) {
var contains, e, length, node, p, range, tn, _k, _l, _len2, _len3, _ref1, _ref2;
range = {};
_ref1 = ['start', 'end'];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
p = _ref1[_k];
try {
node = Range.nodeFromXPath(this[p], root);
} catch (_error) {
e = _error;
throw new Range.RangeError(p, ("Error while finding " + p + " node: " + this[p] + ": ") + e, e);
}
if (!node) {
throw new Range.RangeError(p, "Couldn't find " + p + " node: " + this[p]);
}
length = 0;
_ref2 = $(node).textNodes();
for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) {
tn = _ref2[_l];
if (length + tn.nodeValue.length >= this[p + 'Offset']) {
range[p + 'Container'] = tn;
range[p + 'Offset'] = this[p + 'Offset'] - length;
break;
} else {
length += tn.nodeValue.length;
}
}
if (range[p + 'Offset'] == null) {
throw new Range.RangeError("" + p + "offset", "Couldn't find offset " + this[p + 'Offset'] + " in element " + this[p]);
}
}
contains = document.compareDocumentPosition == null ? function(a, b) {
return a.contains(b);
} : function(a, b) {
return a.compareDocumentPosition(b) & 16;
};
$(range.startContainer).parents().each(function() {
if (contains(this, range.endContainer)) {
range.commonAncestorContainer = this;
return false;
}
});
return new Range.BrowserRange(range).normalize(root);
};
SerializedRange.prototype.serialize = function(root, ignoreSelector) {
return this.normalize(root).serialize(root, ignoreSelector);
};
SerializedRange.prototype.toObject = function() {
return {
start: this.start,
startOffset: this.startOffset,
end: this.end,
endOffset: this.endOffset
};
};
return SerializedRange;
})();
util = {
uuid: (function() {
var counter;
counter = 0;
return function() {
return counter++;
};
})(),
getGlobal: function() {
return (function() {
return this;
})();
},
maxZIndex: function($elements) {
var all, el;
all = (function() {
var _k, _len2, _results;
_results = [];
for (_k = 0, _len2 = $elements.length; _k < _len2; _k++) {
el = $elements[_k];
if ($(el).css('position') === 'static') {
_results.push(-1);
} else {
_results.push(parseInt($(el).css('z-index'), 10) || -1);
}
}
return _results;
})();
return Math.max.apply(Math, all);
},
mousePosition: function(e, offsetEl) {
var offset;
offset = $(offsetEl).position();
return {
top: e.pageY - offset.top,
left: e.pageX - offset.left
};
},
preventEventDefault: function(event) {
return event != null ? typeof event.preventDefault === "function" ? event.preventDefault() : void 0 : void 0;
}
};
_Annotator = this.Annotator;
Annotator = (function(_super) {
__extends(Annotator, _super);
Annotator.prototype.events = {
".annotator-adder button click": "onAdderClick",
".annotator-adder button mousedown": "onAdderMousedown",
".annotator-hl mouseover": "onHighlightMouseover",
".annotator-hl mouseout": "startViewerHideTimer"
};
Annotator.prototype.html = {
adder: '<div class="annotator-adder"><button>' + _t('Annotate') + '</button></div>',
wrapper: '<div class="annotator-wrapper"></div>'
};
Annotator.prototype.options = {
readOnly: false
};
Annotator.prototype.plugins = {};
Annotator.prototype.editor = null;
Annotator.prototype.viewer = null;
Annotator.prototype.selectedRanges = null;
Annotator.prototype.mouseIsDown = false;
Annotator.prototype.ignoreMouseup = false;
Annotator.prototype.viewerHideTimer = null;
function Annotator(element, options) {
this.onDeleteAnnotation = __bind(this.onDeleteAnnotation, this);
this.onEditAnnotation = __bind(this.onEditAnnotation, this);
this.onAdderClick = __bind(this.onAdderClick, this);
this.onAdderMousedown = __bind(this.onAdderMousedown, this);
this.onHighlightMouseover = __bind(this.onHighlightMouseover, this);
this.checkForEndSelection = __bind(this.checkForEndSelection, this);
this.checkForStartSelection = __bind(this.checkForStartSelection, this);
this.clearViewerHideTimer = __bind(this.clearViewerHideTimer, this);
this.startViewerHideTimer = __bind(this.startViewerHideTimer, this);
this.showViewer = __bind(this.showViewer, this);
this.onEditorSubmit = __bind(this.onEditorSubmit, this);
this.onEditorHide = __bind(this.onEditorHide, this);
this.showEditor = __bind(this.showEditor, this); Annotator.__super__.constructor.apply(this, arguments);
this.plugins = {};
if (!Annotator.supported()) {
return this;
}
if (!this.options.readOnly) {
this._setupDocumentEvents();
}
this._setupWrapper()._setupViewer()._setupEditor();
this._setupDynamicStyle();
this.adder = $(this.html.adder).appendTo(this.wrapper).hide();
}
Annotator.prototype._setupWrapper = function() {
this.wrapper = $(this.html.wrapper);
this.element.find('script').remove();
this.element.wrapInner(this.wrapper);
this.wrapper = this.element.find('.annotator-wrapper');
return this;
};
Annotator.prototype._setupViewer = function() {
var _this = this;
this.viewer = new Annotator.Viewer({
readOnly: this.options.readOnly
});
this.viewer.hide().on("edit", this.onEditAnnotation).on("delete", this.onDeleteAnnotation).addField({
load: function(field, annotation) {
if (annotation.text) {
$(field).escape(annotation.text);
} else {
$(field).html("<i>" + (_t('No Comment')) + "</i>");
}
return _this.publish('annotationViewerTextField', [field, annotation]);
}
}).element.appendTo(this.wrapper).bind({
"mouseover": this.clearViewerHideTimer,
"mouseout": this.startViewerHideTimer
});
return this;
};
Annotator.prototype._setupEditor = function() {
this.editor = new Annotator.Editor();
this.editor.hide().on('hide', this.onEditorHide).on('save', this.onEditorSubmit).addField({
type: 'textarea',
label: _t('Comments') + '\u2026',
load: function(field, annotation) {
return $(field).find('textarea').val(annotation.text || '');
},
submit: function(field, annotation) {
return annotation.text = $(field).find('textarea').val();
}
});
this.editor.element.appendTo(this.wrapper);
return this;
};
Annotator.prototype._setupDocumentEvents = function() {
$(document).bind({
"mouseup": this.checkForEndSelection,
"mousedown": this.checkForStartSelection
});
return this;
};
Annotator.prototype._setupDynamicStyle = function() {
var max, sel, style, x;
style = $('#annotator-dynamic-style');
if (!style.length) {
style = $('<style id="annotator-dynamic-style"></style>').appendTo(document.head);
}
sel = '*' + ((function() {
var _k, _len2, _ref1, _results;
_ref1 = ['adder', 'outer', 'notice', 'filter'];
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
x = _ref1[_k];
_results.push(":not(.annotator-" + x + ")");
}
return _results;
})()).join('');
max = util.maxZIndex($(document.body).find(sel));
max = Math.max(max, 1000);
style.text([".annotator-adder, .annotator-outer, .annotator-notice {", " z-index: " + (max + 20) + ";", "}", ".annotator-filter {", " z-index: " + (max + 10) + ";", "}"].join("\n"));
return this;
};
Annotator.prototype.getSelectedRanges = function() {
var browserRange, i, normedRange, r, ranges, rangesToIgnore, selection, _k, _len2;
selection = util.getGlobal().getSelection();
ranges = [];
rangesToIgnore = [];
if (!selection.isCollapsed) {
ranges = (function() {
var _k, _ref1, _results;
_results = [];
for (i = _k = 0, _ref1 = selection.rangeCount; 0 <= _ref1 ? _k < _ref1 : _k > _ref1; i = 0 <= _ref1 ? ++_k : --_k) {
r = selection.getRangeAt(i);
browserRange = new Range.BrowserRange(r);
normedRange = browserRange.normalize().limit(this.wrapper[0]);
if (normedRange === null) {
rangesToIgnore.push(r);
}
_results.push(normedRange);
}
return _results;
}).call(this);
selection.removeAllRanges();
}
for (_k = 0, _len2 = rangesToIgnore.length; _k < _len2; _k++) {
r = rangesToIgnore[_k];
selection.addRange(r);
}
return $.grep(ranges, function(range) {
if (range) {
selection.addRange(range.toRange());
}
return range;
});
};
Annotator.prototype.createAnnotation = function() {
var annotation;
annotation = {};
this.publish('beforeAnnotationCreated', [annotation]);
return annotation;
};
Annotator.prototype.setupAnnotation = function(annotation) {
var e, normed, normedRanges, r, root, _k, _l, _len2, _len3, _ref1;
root = this.wrapper[0];
annotation.ranges || (annotation.ranges = this.selectedRanges);
normedRanges = [];
_ref1 = annotation.ranges;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
r = _ref1[_k];
try {
normedRanges.push(Range.sniff(r).normalize(root));
} catch (_error) {
e = _error;
if (e instanceof Range.RangeError) {
this.publish('rangeNormalizeFail', [annotation, r, e]);
} else {
throw e;
}
}
}
annotation.quote = [];
annotation.ranges = [];
annotation.highlights = [];
for (_l = 0, _len3 = normedRanges.length; _l < _len3; _l++) {
normed = normedRanges[_l];
annotation.quote.push($.trim(normed.text()));
annotation.ranges.push(normed.serialize(this.wrapper[0], '.annotator-hl'));
$.merge(annotation.highlights, this.highlightRange(normed));
}
annotation.quote = annotation.quote.join(' / ');
$(annotation.highlights).data('annotation', annotation);
return annotation;
};
Annotator.prototype.updateAnnotation = function(annotation) {
this.publish('beforeAnnotationUpdated', [annotation]);
this.publish('annotationUpdated', [annotation]);
return annotation;
};
Annotator.prototype.deleteAnnotation = function(annotation) {
var child, h, _k, _len2, _ref1;
if (annotation.highlights != null) {
_ref1 = annotation.highlights;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
h = _ref1[_k];
if (!(h.parentNode != null)) {
continue;
}
child = h.childNodes[0];
$(h).replaceWith(h.childNodes);
}
}
this.publish('annotationDeleted', [annotation]);
return annotation;
};
Annotator.prototype.loadAnnotations = function(annotations) {
var clone, loader,
_this = this;
if (annotations == null) {
annotations = [];
}
loader = function(annList) {
var n, now, _k, _len2;
if (annList == null) {
annList = [];
}
now = annList.splice(0, 10);
for (_k = 0, _len2 = now.length; _k < _len2; _k++) {
n = now[_k];
_this.setupAnnotation(n);
}
if (annList.length > 0) {
return setTimeout((function() {
return loader(annList);
}), 10);
} else {
return _this.publish('annotationsLoaded', [clone]);
}
};
clone = annotations.slice();
if (annotations.length) {
loader(annotations);
}
return this;
};
Annotator.prototype.dumpAnnotations = function() {
if (this.plugins['Store']) {
return this.plugins['Store'].dumpAnnotations();
} else {
console.warn(_t("Can't dump annotations without Store plugin."));
return false;
}
};
Annotator.prototype.highlightRange = function(normedRange, cssClass) {
var hl, node, white, _k, _len2, _ref1, _results;
if (cssClass == null) {
cssClass = 'annotator-hl';
}
white = /^\s*$/;
hl = $("<span class='" + cssClass + "'></span>");
_ref1 = normedRange.textNodes();
_results = [];
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
node = _ref1[_k];
if (!white.test(node.nodeValue)) {
_results.push($(node).wrapAll(hl).parent().show()[0]);
}
}
return _results;
};
Annotator.prototype.highlightRanges = function(normedRanges, cssClass) {
var highlights, r, _k, _len2;
if (cssClass == null) {
cssClass = 'annotator-hl';
}
highlights = [];
for (_k = 0, _len2 = normedRanges.length; _k < _len2; _k++) {
r = normedRanges[_k];
$.merge(highlights, this.highlightRange(r, cssClass));
}
return highlights;
};
Annotator.prototype.addPlugin = function(name, options) {
var klass, _base;
if (this.plugins[name]) {
console.error(_t("You cannot have more than one instance of any plugin."));
} else {
klass = Annotator.Plugin[name];
if (typeof klass === 'function') {
this.plugins[name] = new klass(this.element[0], options);
this.plugins[name].annotator = this;
if (typeof (_base = this.plugins[name]).pluginInit === "function") {
_base.pluginInit();
}
} else {
console.error(_t("Could not load ") + name + _t(" plugin. Have you included the appropriate <script> tag?"));
}
}
return this;
};
Annotator.prototype.showEditor = function(annotation, location) {
this.editor.element.css(location);
this.editor.load(annotation);
this.publish('annotationEditorShown', [this.editor, annotation]);
return this;
};
Annotator.prototype.onEditorHide = function() {
this.publish('annotationEditorHidden', [this.editor]);
return this.ignoreMouseup = false;
};
Annotator.prototype.onEditorSubmit = function(annotation) {
return this.publish('annotationEditorSubmit', [this.editor, annotation]);
};
Annotator.prototype.showViewer = function(annotations, location) {
this.viewer.element.css(location);
this.viewer.load(annotations);
return this.publish('annotationViewerShown', [this.viewer, annotations]);
};
Annotator.prototype.startViewerHideTimer = function() {
if (!this.viewerHideTimer) {
return this.viewerHideTimer = setTimeout(this.viewer.hide, 250);
}
};
Annotator.prototype.clearViewerHideTimer = function() {
clearTimeout(this.viewerHideTimer);
return this.viewerHideTimer = false;
};
Annotator.prototype.checkForStartSelection = function(event) {
if (!(event && this.isAnnotator(event.target))) {
this.startViewerHideTimer();
return this.mouseIsDown = true;
}
};
Annotator.prototype.checkForEndSelection = function(event) {
var container, range, _k, _len2, _ref1;
this.mouseIsDown = false;
if (this.ignoreMouseup) {
return;
}
this.selectedRanges = this.getSelectedRanges();
_ref1 = this.selectedRanges;
for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
range = _ref1[_k];
container = range.commonAncestor;
if ($(container).hasClass('annotator-hl')) {
container = $(container).parents('[class^=annotator-hl]')[0];
}
if (this.isAnnotator(container)) {
return;
}
}
if (event && this.selectedRanges.length) {
return this.adder.css(util.mousePosition(event, this.wrapper[0])).show();
} else {
return this.adder.hide();
}
};
Annotator.prototype.isAnnotator = function(element) {
return !!$(element).parents().andSelf().filter('[class^=annotator-]').not(this.wrapper).length;
};
Annotator.prototype.onHighlightMouseover = function(event) {
var annotations;
this.clearViewerHideTimer();
if (this.mouseIsDown || this.viewer.isShown()) {
return false;
}
annotations = $(event.target).parents('.annotator-hl').andSelf().map(function() {
return $(this).data("annotation");
});
return this.showViewer($.makeArray(annotations), util.mousePosition(event, this.wrapper[0]));
};
Annotator.prototype.onAdderMousedown = function(event) {
if (event != null) {
event.preventDefault();
}
return this.ignoreMouseup = true;
};
Annotator.prototype.onAdderClick = function(event) {
var annotation, cancel, cleanup, position, save,
_this = this;
if (event != null) {
event.preventDefault();
}
position = this.adder.position();
this.adder.hide();
annotation = this.setupAnnotation(this.createAnnotation());
$(annotation.highlights).addClass('annotator-hl-temporary');
save = function() {
cleanup();
$(annotation.highlights).removeClass('annotator-hl-temporary');
return _this.publish('annotationCreated', [annotation]);
};
cancel = function() {
cleanup();
return _this.deleteAnnotation(annotation);
};
cleanup = function() {
_this.unsubscribe('annotationEditorHidden', cancel);
return _this.unsubscribe('annotationEditorSubmit', save);
};
this.subscribe('annotationEditorHidden', cancel);
this.subscribe('annotationEditorSubmit', save);
return this.showEditor(annotation, position);
};
Annotator.prototype.onEditAnnotation = function(annotation) {
var cleanup, offset, update,
_this = this;
offset = this.viewer.element.position();
update = function() {
cleanup();
return _this.updateAnnotation(annotation);
};
cleanup = function() {
_this.unsubscribe('annotationEditorHidden', cleanup);
return _this.unsubscribe('annotationEditorSubmit', update);
};
this.subscribe('annotationEditorHidden', cleanup);
this.subscribe('annotationEditorSubmit', update);
this.viewer.hide();
return this.showEditor(annotation, offset);
};
Annotator.prototype.onDeleteAnnotation = function(annotation) {
this.viewer.hide();
return this.deleteAnnotation(annotation);
};
return Annotator;
})(Delegator);
Annotator.Plugin = (function(_super) {
__extends(Plugin, _super);
function Plugin(element, options) {
Plugin.__super__.constructor.apply(this, arguments);
}
Plugin.prototype.pluginInit = function() {};
return Plugin;
})(Delegator);
g = util.getGlobal();
if (((_ref1 = g.document) != null ? _ref1.evaluate : void 0) == null) {
$.getScript('http://assets.annotateit.org/vendor/xpath.min.js');
}
if (g.getSelection == null) {
$.getScript('http://assets.annotateit.org/vendor/ierange.min.js');
}
if (g.JSON == null) {
$.getScript('http://assets.annotateit.org/vendor/json2.min.js');
}
Annotator.$ = $;
Annotator.Delegator = Delegator;
Annotator.Range = Range;
Annotator._t = _t;
Annotator.supported = function() {
return (function() {
return !!this.getSelection;
})();
};
Annotator.noConflict = function() {
util.getGlobal().Annotator = _Annotator;
return this;
};
$.plugin('annotator', Annotator);
this.Annotator = Annotator;
Annotator.Widget = (function(_super) {
__extends(Widget, _super);
Widget.prototype.classes = {
hide: 'annotator-hide',
invert: {
x: 'annotator-invert-x',
y: 'annotator-invert-y'
}
};
function Widget(element, options) {
Widget.__super__.constructor.apply(this, arguments);
this.classes = $.extend({}, Annotator.Widget.prototype.classes, this.classes);
}
Widget.prototype.checkOrientation = function() {
var current, offset, viewport, widget, window;
this.resetOrientation();
window = $(util.getGlobal());
widget = this.element.children(":first");
offset = widget.offset();
viewport = {
top: window.scrollTop(),
right: window.width() + window.scrollLeft()
};
current = {
top: offset.top,
right: offset.left + widget.width()
};
if ((current.top - viewport.top) < 0) {
this.invertY();
}
if ((current.right - viewport.right) > 0) {
this.invertX();
}
return this;
};
Widget.prototype.resetOrientation = function() {
this.element.removeClass(this.classes.invert.x).removeClass(this.classes.invert.y);
return this;
};
Widget.prototype.invertX = function() {
this.element.addClass(this.classes.invert.x);
return this;
};
Widget.prototype.invertY = function() {
this.element.addClass(this.classes.invert.y);
return this;
};
Widget.prototype.isInvertedY = function() {
return this.element.hasClass(this.classes.invert.y);
};
Widget.prototype.isInvertedX = function() {
return this.element.hasClass(this.classes.invert.x);
};
return Widget;
})(Delegator);
Annotator.Editor = (function(_super) {
__extends(Editor, _super);
Editor.prototype.events = {
"form submit": "submit",
".annotator-save click": "submit",
".annotator-cancel click": "hide",
".annotator-cancel mouseover": "onCancelButtonMouseover",
"textarea keydown": "processKeypress"
};
Editor.prototype.classes = {
hide: 'annotator-hide',
focus: 'annotator-focus'
};
Editor.prototype.html = "<div class=\"annotator-outer annotator-editor\">\n <form class=\"annotator-widget\">\n <ul class=\"annotator-listing\"></ul>\n <div class=\"annotator-controls\">\n <a href=\"#cancel\" class=\"annotator-cancel\">" + _t('Cancel') + "</a>\n<a href=\"#save\" class=\"annotator-save annotator-focus\">" + _t('Save') + "</a>\n </div>\n </form>\n</div>";
Editor.prototype.options = {};
function Editor(options) {
this.onCancelButtonMouseover = __bind(this.onCancelButtonMouseover, this);
this.processKeypress = __bind(this.processKeypress, this);
this.submit = __bind(this.submit, this);
this.load = __bind(this.load, this);
this.hide = __bind(this.hide, this);
this.show = __bind(this.show, this); Editor.__super__.constructor.call(this, $(this.html)[0], options);
this.fields = [];
this.annotation = {};
}
Editor.prototype.show = function(event) {
util.preventEventDefault(event);
this.element.removeClass(this.classes.hide);
this.element.find('.annotator-save').addClass(this.classes.focus);
this.checkOrientation();
this.element.find(":input:first").focus();
this.setupDraggables();
return this.publish('show');
};
Editor.prototype.hide = function(event) {
util.preventEventDefault(event);
this.element.addClass(this.classes.hide);
return this.publish('hide');
};
Editor.prototype.load = function(annotation) {
var field, _k, _len2, _ref2;
this.annotation = annotation;
this.publish('load', [this.annotation]);
_ref2 = this.fields;
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
field = _ref2[_k];
field.load(field.element, this.annotation);
}
return this.show();
};
Editor.prototype.submit = function(event) {
var field, _k, _len2, _ref2;
util.preventEventDefault(event);
_ref2 = this.fields;
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
field = _ref2[_k];
field.submit(field.element, this.annotation);
}
this.publish('save', [this.annotation]);
return this.hide();
};
Editor.prototype.addField = function(options) {
var element, field, input;
field = $.extend({
id: 'annotator-field-' + util.uuid(),
type: 'input',
label: '',
load: function() {},
submit: function() {}
}, options);
input = null;
element = $('<li class="annotator-item" />');
field.element = element[0];
switch (field.type) {
case 'textarea':
input = $('<textarea />');
break;
case 'input':
case 'checkbox':
input = $('<input />');
}
element.append(input);
input.attr({
id: field.id,
placeholder: field.label
});
if (field.type === 'checkbox') {
input[0].type = 'checkbox';
element.addClass('annotator-checkbox');
element.append($('<label />', {
"for": field.id,
html: field.label
}));
}
this.element.find('ul:first').append(element);
this.fields.push(field);
return field.element;
};
Editor.prototype.checkOrientation = function() {
var controls, list;
Editor.__super__.checkOrientation.apply(this, arguments);
list = this.element.find('ul');
controls = this.element.find('.annotator-controls');
if (this.element.hasClass(this.classes.invert.y)) {
controls.insertBefore(list);
} else if (controls.is(':first-child')) {
controls.insertAfter(list);
}
return this;
};
Editor.prototype.processKeypress = function(event) {
if (event.keyCode === 27) {
return this.hide();
} else if (event.keyCode === 13 && !event.shiftKey) {
return this.submit();
}
};
Editor.prototype.onCancelButtonMouseover = function() {
return this.element.find('.' + this.classes.focus).removeClass(this.classes.focus);
};
Editor.prototype.setupDraggables = function() {
var classes, controls, cornerItem, editor, mousedown, onMousedown, onMousemove, onMouseup, resize, textarea, throttle,
_this = this;
this.element.find('.annotator-resize').remove();
if (this.element.hasClass(this.classes.invert.y)) {
cornerItem = this.element.find('.annotator-item:last');
} else {
cornerItem = this.element.find('.annotator-item:first');
}
if (cornerItem) {
$('<span class="annotator-resize"></span>').appendTo(cornerItem);
}
mousedown = null;
classes = this.classes;
editor = this.element;
textarea = null;
resize = editor.find('.annotator-resize');
controls = editor.find('.annotator-controls');
throttle = false;
onMousedown = function(event) {
if (event.target === this) {
mousedown = {
element: this,
top: event.pageY,
left: event.pageX
};
textarea = editor.find('textarea:first');
$(window).bind({
'mouseup.annotator-editor-resize': onMouseup,
'mousemove.annotator-editor-resize': onMousemove
});
return event.preventDefault();
}
};
onMouseup = function() {
mousedown = null;
return $(window).unbind('.annotator-editor-resize');
};
onMousemove = function(event) {
var diff, directionX, directionY, height, width;
if (mousedown && throttle === false) {
diff = {
top: event.pageY - mousedown.top,
left: event.pageX - mousedown.left
};
if (mousedown.element === resize[0]) {
height = textarea.outerHeight();
width = textarea.outerWidth();
directionX = editor.hasClass(classes.invert.x) ? -1 : 1;
directionY = editor.hasClass(classes.invert.y) ? 1 : -1;
textarea.height(height + (diff.top * directionY));
textarea.width(width + (diff.left * directionX));
if (textarea.outerHeight() !== height) {
mousedown.top = event.pageY;
}
if (textarea.outerWidth() !== width) {
mousedown.left = event.pageX;
}
} else if (mousedown.element === controls[0]) {
editor.css({
top: parseInt(editor.css('top'), 10) + diff.top,
left: parseInt(editor.css('left'), 10) + diff.left
});
mousedown.top = event.pageY;
mousedown.left = event.pageX;
}
throttle = true;
return setTimeout(function() {
return throttle = false;
}, 1000 / 60);
}
};
resize.bind('mousedown', onMousedown);
return controls.bind('mousedown', onMousedown);
};
return Editor;
})(Annotator.Widget);
Annotator.Viewer = (function(_super) {
__extends(Viewer, _super);
Viewer.prototype.events = {
".annotator-edit click": "onEditClick",
".annotator-delete click": "onDeleteClick"
};
Viewer.prototype.classes = {
hide: 'annotator-hide',
showControls: 'annotator-visible'
};
Viewer.prototype.html = {
element: "<div class=\"annotator-outer annotator-viewer\">\n <ul class=\"annotator-widget annotator-listing\"></ul>\n</div>",
item: "<li class=\"annotator-annotation annotator-item\">\n <span class=\"annotator-controls\">\n <a href=\"#\" title=\"View as webpage\" class=\"annotator-link\">View as webpage</a>\n <button title=\"Edit\" class=\"annotator-edit\">Edit</button>\n <button title=\"Delete\" class=\"annotator-delete\">Delete</button>\n </span>\n</li>"
};
Viewer.prototype.options = {
readOnly: false
};
function Viewer(options) {
this.onDeleteClick = __bind(this.onDeleteClick, this);
this.onEditClick = __bind(this.onEditClick, this);
this.load = __bind(this.load, this);
this.hide = __bind(this.hide, this);
this.show = __bind(this.show, this); Viewer.__super__.constructor.call(this, $(this.html.element)[0], options);
this.item = $(this.html.item)[0];
this.fields = [];
this.annotations = [];
}
Viewer.prototype.show = function(event) {
var controls,
_this = this;
util.preventEventDefault(event);
controls = this.element.find('.annotator-controls').addClass(this.classes.showControls);
setTimeout((function() {
return controls.removeClass(_this.classes.showControls);
}), 500);
this.element.removeClass(this.classes.hide);
return this.checkOrientation().publish('show');
};
Viewer.prototype.isShown = function() {
return !this.element.hasClass(this.classes.hide);
};
Viewer.prototype.hide = function(event) {
util.preventEventDefault(event);
this.element.addClass(this.classes.hide);
return this.publish('hide');
};
Viewer.prototype.load = function(annotations) {
var annotation, controller, controls, del, edit, element, field, item, link, links, list, _k, _l, _len2, _len3, _ref2, _ref3;
this.annotations = annotations || [];
list = this.element.find('ul:first').empty();
_ref2 = this.annotations;
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
annotation = _ref2[_k];
item = $(this.item).clone().appendTo(list).data('annotation', annotation);
controls = item.find('.annotator-controls');
link = controls.find('.annotator-link');
edit = controls.find('.annotator-edit');
del = controls.find('.annotator-delete');
links = new LinkParser(annotation.links || []).get('alternate', {
'type': 'text/html'
});
if (links.length === 0 || (links[0].href == null)) {
link.remove();
} else {
link.attr('href', links[0].href);
}
if (this.options.readOnly) {
edit.remove();
del.remove();
} else {
controller = {
showEdit: function() {
return edit.removeAttr('disabled');
},
hideEdit: function() {
return edit.attr('disabled', 'disabled');
},
showDelete: function() {
return del.removeAttr('disabled');
},
hideDelete: function() {
return del.attr('disabled', 'disabled');
}
};
}
_ref3 = this.fields;
for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
field = _ref3[_l];
element = $(field.element).clone().appendTo(item)[0];
field.load(element, annotation, controller);
}
}
this.publish('load', [this.annotations]);
return this.show();
};
Viewer.prototype.addField = function(options) {
var field;
field = $.extend({
load: function() {}
}, options);
field.element = $('<div />')[0];
this.fields.push(field);
field.element;
return this;
};
Viewer.prototype.onEditClick = function(event) {
return this.onButtonClick(event, 'edit');
};
Viewer.prototype.onDeleteClick = function(event) {
return this.onButtonClick(event, 'delete');
};
Viewer.prototype.onButtonClick = function(event, type) {
var item;
item = $(event.target).parents('.annotator-annotation');
return this.publish(type, [item.data('annotation')]);
};
return Viewer;
})(Annotator.Widget);
LinkParser = (function() {
function LinkParser(data) {
this.data = data;
}
LinkParser.prototype.get = function(rel, cond) {
var d, k, keys, match, v, _k, _len2, _ref2, _results;
if (cond == null) {
cond = {};
}
cond = $.extend({}, cond, {
rel: rel
});
keys = (function() {
var _results;
_results = [];
for (k in cond) {
if (!__hasProp.call(cond, k)) continue;
v = cond[k];
_results.push(k);
}
return _results;
})();
_ref2 = this.data;
_results = [];
for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
d = _ref2[_k];
match = keys.reduce((function(m, k) {
return m && (d[k] === cond[k]);
}), true);
if (match) {
_results.push(d);
} else {
continue;
}
}
return _results;
};
return LinkParser;
})();
Annotator = Annotator || {};
Annotator.Notification = (function(_super) {
__extends(Notification, _super);
Notification.prototype.events = {
"click": "hide"
};
Notification.prototype.options = {
html: "<div class='annotator-notice'></div>",
classes: {
show: "annotator-notice-show",
info: "annotator-notice-info",
success: "annotator-notice-success",
error: "annotator-notice-error"
}
};
function Notification(options) {
this.hide = __bind(this.hide, this);
this.show = __bind(this.show, this); Notification.__super__.constructor.call(this, $(this.options.html).appendTo(document.body)[0], options);
}
Notification.prototype.show = function(message, status) {
if (status == null) {
status = Annotator.Notification.INFO;
}
$(this.element).addClass(this.options.classes.show).addClass(this.options.classes[status]).escape(message || "");
setTimeout(this.hide, 5000);
return this;
};
Notification.prototype.hide = function() {
$(this.element).removeClass(this.options.classes.show);
return this;
};
return Notification;
})(Delegator);
Annotator.Notification.INFO = 'show';
Annotator.Notification.SUCCESS = 'success';
Annotator.Notification.ERROR = 'error';
$(function() {
var notification;
notification = new Annotator.Notification;
Annotator.showNotification = notification.show;
return Annotator.hideNotification = notification.hide;
});
}).call(this);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,273 @@
/*
** Annotator 1.2.6-dev-dc18206
** https://github.com/okfn/annotator/
**
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2013-05-16 18:02:02Z
*/
(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
Annotator.Plugin.Store = (function(_super) {
__extends(Store, _super);
Store.prototype.events = {
'annotationCreated': 'annotationCreated',
'annotationDeleted': 'annotationDeleted',
'annotationUpdated': 'annotationUpdated'
};
Store.prototype.options = {
annotationData: {},
emulateHTTP: false,
loadFromSearch: false,
prefix: '/store',
urls: {
create: '/annotations',
read: '/annotations/:id',
update: '/annotations/:id',
destroy: '/annotations/:id',
search: '/search'
}
};
function Store(element, options) {
this._onError = __bind(this._onError, this);
this._onLoadAnnotationsFromSearch = __bind(this._onLoadAnnotationsFromSearch, this);
this._onLoadAnnotations = __bind(this._onLoadAnnotations, this);
this._getAnnotations = __bind(this._getAnnotations, this); Store.__super__.constructor.apply(this, arguments);
this.annotations = [];
}
Store.prototype.pluginInit = function() {
if (!Annotator.supported()) {
return;
}
if (this.annotator.plugins.Auth) {
return this.annotator.plugins.Auth.withToken(this._getAnnotations);
} else {
return this._getAnnotations();
}
};
Store.prototype._getAnnotations = function() {
if (this.options.loadFromSearch) {
return this.loadAnnotationsFromSearch(this.options.loadFromSearch);
} else {
return this.loadAnnotations();
}
};
Store.prototype.annotationCreated = function(annotation) {
var _this = this;
if (__indexOf.call(this.annotations, annotation) < 0) {
this.registerAnnotation(annotation);
return this._apiRequest('create', annotation, function(data) {
if (data.id == null) {
console.warn(Annotator._t("Warning: No ID returned from server for annotation "), annotation);
}
return _this.updateAnnotation(annotation, data);
});
} else {
return this.updateAnnotation(annotation, {});
}
};
Store.prototype.annotationUpdated = function(annotation) {
var _this = this;
if (__indexOf.call(this.annotations, annotation) >= 0) {
return this._apiRequest('update', annotation, (function(data) {
return _this.updateAnnotation(annotation, data);
}));
}
};
Store.prototype.annotationDeleted = function(annotation) {
var _this = this;
if (__indexOf.call(this.annotations, annotation) >= 0) {
return this._apiRequest('destroy', annotation, (function() {
return _this.unregisterAnnotation(annotation);
}));
}
};
Store.prototype.registerAnnotation = function(annotation) {
return this.annotations.push(annotation);
};
Store.prototype.unregisterAnnotation = function(annotation) {
return this.annotations.splice(this.annotations.indexOf(annotation), 1);
};
Store.prototype.updateAnnotation = function(annotation, data) {
if (__indexOf.call(this.annotations, annotation) < 0) {
console.error(Annotator._t("Trying to update unregistered annotation!"));
} else {
$.extend(annotation, data);
}
return $(annotation.highlights).data('annotation', annotation);
};
Store.prototype.loadAnnotations = function() {
return this._apiRequest('read', null, this._onLoadAnnotations);
};
Store.prototype._onLoadAnnotations = function(data) {
if (data == null) {
data = [];
}
this.annotations = this.annotations.concat(data);
return this.annotator.loadAnnotations(data.slice());
};
Store.prototype.loadAnnotationsFromSearch = function(searchOptions) {
return this._apiRequest('search', searchOptions, this._onLoadAnnotationsFromSearch);
};
Store.prototype._onLoadAnnotationsFromSearch = function(data) {
if (data == null) {
data = {};
}
return this._onLoadAnnotations(data.rows || []);
};
Store.prototype.dumpAnnotations = function() {
var ann, _i, _len, _ref, _results;
_ref = this.annotations;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
ann = _ref[_i];
_results.push(JSON.parse(this._dataFor(ann)));
}
return _results;
};
Store.prototype._apiRequest = function(action, obj, onSuccess) {
var id, options, request, url;
id = obj && obj.id;
url = this._urlFor(action, id);
options = this._apiRequestOptions(action, obj, onSuccess);
request = $.ajax(url, options);
request._id = id;
request._action = action;
return request;
};
Store.prototype._apiRequestOptions = function(action, obj, onSuccess) {
var data, method, opts;
method = this._methodFor(action);
opts = {
type: method,
headers: this.element.data('annotator:headers'),
dataType: "json",
success: onSuccess || function() {},
error: this._onError
};
if (this.options.emulateHTTP && (method === 'PUT' || method === 'DELETE')) {
opts.headers = $.extend(opts.headers, {
'X-HTTP-Method-Override': method
});
opts.type = 'POST';
}
if (action === "search") {
opts = $.extend(opts, {
data: obj
});
return opts;
}
data = obj && this._dataFor(obj);
if (this.options.emulateJSON) {
opts.data = {
json: data
};
if (this.options.emulateHTTP) {
opts.data._method = method;
}
return opts;
}
opts = $.extend(opts, {
data: data,
contentType: "application/json; charset=utf-8"
});
return opts;
};
Store.prototype._urlFor = function(action, id) {
var url;
url = this.options.prefix != null ? this.options.prefix : '';
url += this.options.urls[action];
url = url.replace(/\/:id/, id != null ? '/' + id : '');
url = url.replace(/:id/, id != null ? id : '');
return url;
};
Store.prototype._methodFor = function(action) {
var table;
table = {
'create': 'POST',
'read': 'GET',
'update': 'PUT',
'destroy': 'DELETE',
'search': 'GET'
};
return table[action];
};
Store.prototype._dataFor = function(annotation) {
var data, highlights;
highlights = annotation.highlights;
delete annotation.highlights;
$.extend(annotation, this.options.annotationData);
data = JSON.stringify(annotation);
if (highlights) {
annotation.highlights = highlights;
}
return data;
};
Store.prototype._onError = function(xhr) {
var action, message;
action = xhr._action;
message = Annotator._t("Sorry we could not ") + action + Annotator._t(" this annotation");
if (xhr._action === 'search') {
message = Annotator._t("Sorry we could not search the store for annotations");
} else if (xhr._action === 'read' && !xhr._id) {
message = Annotator._t("Sorry we could not ") + action + Annotator._t(" the annotations from the store");
}
switch (xhr.status) {
case 401:
message = Annotator._t("Sorry you are not allowed to ") + action + Annotator._t(" this annotation");
break;
case 404:
message = Annotator._t("Sorry we could not connect to the annotations store");
break;
case 500:
message = Annotator._t("Sorry something went wrong with the annotation store");
}
Annotator.showNotification(message, Annotator.Notification.ERROR);
return console.error(Annotator._t("API request failed:") + (" '" + xhr.status + "'"));
};
return Store;
})(Annotator.Plugin);
}).call(this);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,136 @@
/*
** Annotator 1.2.6-dev-dc18206
** https://github.com/okfn/annotator/
**
** Copyright 2012 Aron Carroll, Rufus Pollock, and Nick Stenning.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/okfn/annotator/blob/master/LICENSE
**
** Built at: 2013-05-16 18:02:02Z
*/
(function() {
var _ref,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Annotator.Plugin.Tags = (function(_super) {
__extends(Tags, _super);
function Tags() {
this.setAnnotationTags = __bind(this.setAnnotationTags, this);
this.updateField = __bind(this.updateField, this); _ref = Tags.__super__.constructor.apply(this, arguments);
return _ref;
}
Tags.prototype.options = {
parseTags: function(string) {
var tags;
string = $.trim(string);
tags = [];
if (string) {
tags = string.split(/\s+/);
}
return tags;
},
stringifyTags: function(array) {
return array.join(" ");
}
};
Tags.prototype.field = null;
Tags.prototype.input = null;
Tags.prototype.pluginInit = function() {
if (!Annotator.supported()) {
return;
}
this.field = this.annotator.editor.addField({
label: Annotator._t('Add some tags here') + '\u2026',
load: this.updateField,
submit: this.setAnnotationTags
});
this.annotator.viewer.addField({
load: this.updateViewer
});
if (this.annotator.plugins.Filter) {
this.annotator.plugins.Filter.addFilter({
label: Annotator._t('Tag'),
property: 'tags',
isFiltered: Annotator.Plugin.Tags.filterCallback
});
}
return this.input = $(this.field).find(':input');
};
Tags.prototype.parseTags = function(string) {
return this.options.parseTags(string);
};
Tags.prototype.stringifyTags = function(array) {
return this.options.stringifyTags(array);
};
Tags.prototype.updateField = function(field, annotation) {
var value;
value = '';
if (annotation.tags) {
value = this.stringifyTags(annotation.tags);
}
return this.input.val(value);
};
Tags.prototype.setAnnotationTags = function(field, annotation) {
return annotation.tags = this.parseTags(this.input.val());
};
Tags.prototype.updateViewer = function(field, annotation) {
field = $(field);
if (annotation.tags && $.isArray(annotation.tags) && annotation.tags.length) {
return field.addClass('annotator-tags').html(function() {
var string;
return string = $.map(annotation.tags, function(tag) {
return '<span class="annotator-tag">' + Annotator.$.escape(tag) + '</span>';
}).join(' ');
});
} else {
return field.remove();
}
};
return Tags;
})(Annotator.Plugin);
Annotator.Plugin.Tags.filterCallback = function(input, tags) {
var keyword, keywords, matches, tag, _i, _j, _len, _len1;
if (tags == null) {
tags = [];
}
matches = 0;
keywords = [];
if (input) {
keywords = input.split(/\s+/g);
for (_i = 0, _len = keywords.length; _i < _len; _i++) {
keyword = keywords[_i];
if (tags.length) {
for (_j = 0, _len1 = tags.length; _j < _len1; _j++) {
tag = tags[_j];
if (tag.indexOf(keyword) !== -1) {
matches += 1;
}
}
}
}
}
return matches === keywords.length;
};
}).call(this);

View File

@@ -0,0 +1 @@
(function(){var _ref,__bind=function(fn,me){return function(){return fn.apply(me,arguments)}},__hasProp={}.hasOwnProperty,__extends=function(child,parent){for(var key in parent){if(__hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child};Annotator.Plugin.Tags=function(_super){__extends(Tags,_super);function Tags(){this.setAnnotationTags=__bind(this.setAnnotationTags,this);this.updateField=__bind(this.updateField,this);_ref=Tags.__super__.constructor.apply(this,arguments);return _ref}Tags.prototype.options={parseTags:function(string){var tags;string=$.trim(string);tags=[];if(string){tags=string.split(/\s+/)}return tags},stringifyTags:function(array){return array.join(" ")}};Tags.prototype.field=null;Tags.prototype.input=null;Tags.prototype.pluginInit=function(){if(!Annotator.supported()){return}this.field=this.annotator.editor.addField({label:Annotator._t("Add some tags here")+"…",load:this.updateField,submit:this.setAnnotationTags});this.annotator.viewer.addField({load:this.updateViewer});if(this.annotator.plugins.Filter){this.annotator.plugins.Filter.addFilter({label:Annotator._t("Tag"),property:"tags",isFiltered:Annotator.Plugin.Tags.filterCallback})}return this.input=$(this.field).find(":input")};Tags.prototype.parseTags=function(string){return this.options.parseTags(string)};Tags.prototype.stringifyTags=function(array){return this.options.stringifyTags(array)};Tags.prototype.updateField=function(field,annotation){var value;value="";if(annotation.tags){value=this.stringifyTags(annotation.tags)}return this.input.val(value)};Tags.prototype.setAnnotationTags=function(field,annotation){return annotation.tags=this.parseTags(this.input.val())};Tags.prototype.updateViewer=function(field,annotation){field=$(field);if(annotation.tags&&$.isArray(annotation.tags)&&annotation.tags.length){return field.addClass("annotator-tags").html(function(){var string;return string=$.map(annotation.tags,function(tag){return'<span class="annotator-tag">'+Annotator.$.escape(tag)+"</span>"}).join(" ")})}else{return field.remove()}};return Tags}(Annotator.Plugin);Annotator.Plugin.Tags.filterCallback=function(input,tags){var keyword,keywords,matches,tag,_i,_j,_len,_len1;if(tags==null){tags=[]}matches=0;keywords=[];if(input){keywords=input.split(/\s+/g);for(_i=0,_len=keywords.length;_i<_len;_i++){keyword=keywords[_i];if(tags.length){for(_j=0,_len1=tags.length;_j<_len1;_j++){tag=tags[_j];if(tag.indexOf(keyword)!==-1){matches+=1}}}}}return matches===keywords.length}}).call(this);

View File

@@ -0,0 +1,152 @@
/**
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
*
* @name timeago
* @version 0.11.4
* @requires jQuery v1.2.3+
* @author Ryan McGeary
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Copyright (c) 2008-2012, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
*/
(function($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) {
return inWords(timestamp);
} else if (typeof timestamp === "string") {
return inWords($.timeago.parse(timestamp));
} else if (typeof timestamp === "number") {
return inWords(new Date(timestamp));
} else {
return inWords($.timeago.datetime(timestamp));
}
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 60000,
allowFuture: false,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "ago",
suffixFromNow: "from now",
seconds: "less than a minute",
minute: "about a minute",
minutes: "%d minutes",
hour: "about an hour",
hours: "about %d hours",
day: "a day",
days: "%d days",
month: "about a month",
months: "%d months",
year: "about a year",
years: "%d years",
wordSeparator: " ",
numbers: []
}
},
inWords: function(distanceMillis) {
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
}
var seconds = Math.abs(distanceMillis) / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 42 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.round(days)) ||
days < 45 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
years < 1.5 && substitute($l.year, 1) ||
substitute($l.years, Math.round(years));
var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator;
return $.trim([prefix, words, suffix].join(separator));
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d+/,""); // remove milliseconds
s = s.replace(/-/,"/").replace(/-/,"/");
s = s.replace(/T/," ").replace(/Z/," UTC");
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
return new Date(s);
},
datetime: function(elem) {
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
return $t.parse(iso8601);
},
isTime: function(elem) {
// jQuery's `is()` doesn't play well with HTML5 in IE
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
}
});
$.fn.timeago = function() {
var self = this;
self.each(refresh);
var $s = $t.settings;
if ($s.refreshMillis > 0) {
setInterval(function() { self.each(refresh); }, $s.refreshMillis);
}
return self;
};
function refresh() {
var data = prepareData(this);
if (!isNaN(data.datetime)) {
$(this).text(inWords(data.datetime));
}
return this;
}
function prepareData(element) {
element = $(element);
if (!element.data("timeago")) {
element.data("timeago", { datetime: $t.datetime(element) });
var text = $.trim(element.text());
if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
element.attr("title", text);
}
}
return element.data("timeago");
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return (new Date().getTime() - date.getTime());
}
// fix for IE6 suckage
document.createElement("abbr");
document.createElement("time");
}(jQuery));

View File

@@ -0,0 +1,152 @@
/**
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
*
* @name timeago
* @version 0.11.4
* @requires jQuery v1.2.3+
* @author Ryan McGeary
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Copyright (c) 2008-2012, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
*/
(function($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) {
return inWords(timestamp);
} else if (typeof timestamp === "string") {
return inWords($.timeago.parse(timestamp));
} else if (typeof timestamp === "number") {
return inWords(new Date(timestamp));
} else {
return inWords($.timeago.datetime(timestamp));
}
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 60000,
allowFuture: false,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "ago",
suffixFromNow: "from now",
seconds: "less than a minute",
minute: "about a minute",
minutes: "%d minutes",
hour: "about an hour",
hours: "about %d hours",
day: "a day",
days: "%d days",
month: "about a month",
months: "%d months",
year: "about a year",
years: "%d years",
wordSeparator: " ",
numbers: []
}
},
inWords: function(distanceMillis) {
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
}
var seconds = Math.abs(distanceMillis) / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 42 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.round(days)) ||
days < 45 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
years < 1.5 && substitute($l.year, 1) ||
substitute($l.years, Math.round(years));
var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator;
return $.trim([prefix, words, suffix].join(separator));
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d+/,""); // remove milliseconds
s = s.replace(/-/,"/").replace(/-/,"/");
s = s.replace(/T/," ").replace(/Z/," UTC");
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
return new Date(s);
},
datetime: function(elem) {
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
return $t.parse(iso8601);
},
isTime: function(elem) {
// jQuery's `is()` doesn't play well with HTML5 in IE
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
}
});
$.fn.timeago = function() {
var self = this;
self.each(refresh);
var $s = $t.settings;
if ($s.refreshMillis > 0) {
setInterval(function() { self.each(refresh); }, $s.refreshMillis);
}
return self;
};
function refresh() {
var data = prepareData(this);
if (!isNaN(data.datetime)) {
$(this).text(inWords(data.datetime));
}
return this;
}
function prepareData(element) {
element = $(element);
if (!element.data("timeago")) {
element.data("timeago", { datetime: $t.datetime(element) });
var text = $.trim(element.text());
if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
element.attr("title", text);
}
}
return element.data("timeago");
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return (new Date().getTime() - date.getTime());
}
// fix for IE6 suckage
document.createElement("abbr");
document.createElement("time");
}(jQuery));

View File

@@ -0,0 +1,75 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Jasmine Test Runner</title>
<link rel="stylesheet" type="text/css" href="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.css">
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine.js"></script>
<script type="text/javascript" src="<%= phantom_jasmine_path %>/vendor/jasmine-1.2.0/jasmine-html.js"></script>
<script type="text/javascript" src="<%= phantom_jasmine_path %>/lib/console-runner.js"></script>
<script type="text/javascript" src="<%= common_coffee_root %>/ajax_prefix.js"></script>
<script type="text/javascript" src="<%= common_coffee_root %>/logger.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jasmine-jquery.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/RequireJS.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.min.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery-ui.min.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.ui.draggable.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.cookie.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/json2.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/underscore-min.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/backbone-min.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.leanModal.min.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/CodeMirror/codemirror.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/jquery.tinymce.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=default"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/jquery.timeago.js"></script>
<script type="text/javascript">
AjaxPrefix.addAjaxPrefix(jQuery, function() {
return "";
});
</script>
<!-- SOURCE FILES -->
<% for src in js_source %>
<script type="text/javascript" src="<%= src %>"></script>
<% end %>
<!-- SPEC FILES -->
<% for src in js_specs %>
<script type="text/javascript" src="<%= src %>"></script>
<% end %>
</head>
<body>
<script type="text/javascript">
var jasmineEnv = jasmine.getEnv();
var htmlReporter = new jasmine.HtmlReporter();
var console_reporter = new jasmine.ConsoleReporter()
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.addReporter(console_reporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
<course org="edX" course="embedded_python" url_name="2013_Spring"/>

View File

@@ -0,0 +1,111 @@
<course>
<chapter url_name="EmbeddedPythonChapter">
<vertical url_name="Homework1">
<problem url_name="schematic_problem">
<schematicresponse>
<center>
<schematic height="500" width="600" parts="g,n,s" analyses="dc,tran"
submit_analyses="{&quot;tran&quot;:[[&quot;Z&quot;,0.0000004,0.0000009,0.0000014,0.0000019,0.0000024,0.0000029,0.0000034,0.000039]]}"
initial_value="[[&quot;w&quot;,[112,96,128,96]],[&quot;w&quot;,[256,96,240,96]],[&quot;w&quot;,[192,96,240,96]],[&quot;s&quot;,[240,96,0],{&quot;color&quot;:&quot;cyan&quot;,&quot;offset&quot;:&quot;&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:3},[&quot;Z&quot;]],[&quot;w&quot;,[32,224,192,224]],[&quot;w&quot;,[96,48,192,48]],[&quot;L&quot;,[256,96,3],{&quot;label&quot;:&quot;Z&quot;,&quot;_json_&quot;:6},[&quot;Z&quot;]],[&quot;r&quot;,[192,48,0],{&quot;name&quot;:&quot;Rpullup&quot;,&quot;r&quot;:&quot;10K&quot;,&quot;_json_&quot;:7},[&quot;1&quot;,&quot;Z&quot;]],[&quot;w&quot;,[32,144,32,192]],[&quot;w&quot;,[32,224,32,192]],[&quot;w&quot;,[48,192,32,192]],[&quot;w&quot;,[32,96,32,144]],[&quot;w&quot;,[48,144,32,144]],[&quot;w&quot;,[32,48,32,96]],[&quot;w&quot;,[48,96,32,96]],[&quot;w&quot;,[32,48,48,48]],[&quot;g&quot;,[32,224,0],{&quot;_json_&quot;:16},[&quot;0&quot;]],[&quot;v&quot;,[96,192,1],{&quot;name&quot;:&quot;VC&quot;,&quot;value&quot;:&quot;square(3,0,250K)&quot;,&quot;_json_&quot;:17},[&quot;C&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,144,1],{&quot;name&quot;:&quot;VB&quot;,&quot;value&quot;:&quot;square(3,0,500K)&quot;,&quot;_json_&quot;:18},[&quot;B&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,96,1],{&quot;name&quot;:&quot;VA&quot;,&quot;value&quot;:&quot;square(3,0,1000K)&quot;,&quot;_json_&quot;:19},[&quot;A&quot;,&quot;0&quot;]],[&quot;v&quot;,[96,48,1],{&quot;name&quot;:&quot;Vpwr&quot;,&quot;value&quot;:&quot;dc(3)&quot;,&quot;_json_&quot;:20},[&quot;1&quot;,&quot;0&quot;]],[&quot;L&quot;,[96,96,2],{&quot;label&quot;:&quot;A&quot;,&quot;_json_&quot;:21},[&quot;A&quot;]],[&quot;w&quot;,[96,96,104,96]],[&quot;L&quot;,[96,144,2],{&quot;label&quot;:&quot;B&quot;,&quot;_json_&quot;:23},[&quot;B&quot;]],[&quot;w&quot;,[96,144,104,144]],[&quot;L&quot;,[96,192,2],{&quot;label&quot;:&quot;C&quot;,&quot;_json_&quot;:25},[&quot;C&quot;]],[&quot;w&quot;,[96,192,104,192]],[&quot;w&quot;,[192,96,192,112]],[&quot;s&quot;,[112,96,0],{&quot;color&quot;:&quot;red&quot;,&quot;offset&quot;:&quot;15&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:28},[&quot;A&quot;]],[&quot;w&quot;,[104,96,112,96]],[&quot;s&quot;,[112,144,0],{&quot;color&quot;:&quot;green&quot;,&quot;offset&quot;:&quot;10&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:30},[&quot;B&quot;]],[&quot;w&quot;,[104,144,112,144]],[&quot;w&quot;,[128,144,112,144]],[&quot;s&quot;,[112,192,0],{&quot;color&quot;:&quot;blue&quot;,&quot;offset&quot;:&quot;5&quot;,&quot;plot offset&quot;:&quot;0&quot;,&quot;_json_&quot;:33},[&quot;C&quot;]],[&quot;w&quot;,[104,192,112,192]],[&quot;w&quot;,[128,192,112,192]],[&quot;view&quot;,0,0,2,&quot;5&quot;,&quot;10&quot;,&quot;10MEG&quot;,null,&quot;100&quot;,&quot;4us&quot;]]"
/>
</center>
<answer type="loncapa/python">
# for a schematic response, submission[i] is the json representation
# of the diagram and analysis results for the i-th schematic tag
def get_tran(json,signal):
for element in json:
if element[0] == 'transient':
return element[1].get(signal,[])
return []
def get_value(at,output):
for (t,v) in output:
if at == t: return v
return None
output = get_tran(submission[0],'Z')
okay = True
# output should be 1, 1, 1, 1, 1, 0, 0, 0
if get_value(0.0000004,output) &lt; 2.7: okay = False;
if get_value(0.0000009,output) &lt; 2.7: okay = False;
if get_value(0.0000014,output) &lt; 2.7: okay = False;
if get_value(0.0000019,output) &lt; 2.7: okay = False;
if get_value(0.0000024,output) &lt; 2.7: okay = False;
if get_value(0.0000029,output) &gt; 0.25: okay = False;
if get_value(0.0000034,output) &gt; 0.25: okay = False;
if get_value(0.0000039,output) &gt; 0.25: okay = False;
correct = ['correct' if okay else 'incorrect']
</answer></schematicresponse>
</problem>
<problem url_name="cfn_problem">
<text>
<script type="text/python" system_path="python_lib">
def test_csv(expect, ans):
# Take out all spaces in expected answer
expect = [i.strip(' ') for i in str(expect).split(',')]
# Take out all spaces in student solution
ans = [i.strip(' ') for i in str(ans).split(',')]
def strip_q(x):
# Strip quotes around strings if students have entered them
stripped_ans = []
for item in x:
if item[0] == "'" and item[-1]=="'":
item = item.strip("'")
elif item[0] == '"' and item[-1] == '"':
item = item.strip('"')
stripped_ans.append(item)
return stripped_ans
return strip_q(expect) == strip_q(ans)
</script>
<ol class="enumerate">
<li>
<pre>
num = 0
while num &lt;= 5:
print(num)
num += 1
print("Outside of loop")
print(num)
</pre>
<p>
<customresponse cfn="test_csv" expect="0, 1, 2, 3, 4, 5, 'Outside of loop', 6">
<textline size="50" correct_answer="0, 1, 2, 3, 4, 5, 'Outside of loop', 6"/>
</customresponse>
</p>
</li>
</ol>
</text>
</problem>
<problem url_name="computed_answer">
<customresponse>
<textline size="5" correct_answer="Xyzzy"/>
<answer type="loncapa/python">
if submission[0] == "Xyzzy":
correct = ['correct']
else:
correct = ['incorrect']
</answer>
</customresponse>
</problem>
</vertical>
</chapter>
</course>

View File

@@ -0,0 +1 @@
<course org="edX" course="embedded_python" url_name="2013_Spring"/>

View File

@@ -19,7 +19,7 @@ from symmath import *
<text>Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 &amp; 1 \\ 1 &amp; 0 \end{matrix} \right] \right) [/mathjax]
and give the resulting \(2 \times 2\) matrix. <br/>
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>. <br/>
[mathjax]U=[/mathjax] <symbolicresponse cfn="symmath_check" answer="[[cos(theta),I*sin(theta)],[I*sin(theta),cos(theta)]]" options="matrix,imaginaryi" id="filenamedogi0VpEBOWedxsymmathresponse_1" state="unsubmitted">
[mathjax]U=[/mathjax] <symbolicresponse cfn="symmath_check" answer="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]" options="matrix,imaginary" id="filenamedogi0VpEBOWedxsymmathresponse_1" state="unsubmitted">
<textline size="80" math="1" response_id="2" answer_id="1" id="filenamedogi0VpEBOWedxsymmathresponse_2_1"/>
</symbolicresponse>
<br/>