Merge branch 'feature/christina/metadata-ui' of github.com:edx/edx-platform into feature/christina/metadata-ui
This commit is contained in:
1
common/.gitignore
vendored
Normal file
1
common/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
jasmine_test_runner.html
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
27
common/djangoapps/mitxmako/tests.py
Normal file
27
common/djangoapps/mitxmako/tests.py
Normal 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)
|
||||
3
common/djangoapps/service_status/__init__.py
Normal file
3
common/djangoapps/service_status/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Stub for a Django app to report the status of various services
|
||||
"""
|
||||
25
common/djangoapps/service_status/tasks.py
Normal file
25
common/djangoapps/service_status/tasks.py
Normal 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
|
||||
47
common/djangoapps/service_status/test.py
Normal file
47
common/djangoapps/service_status/test.py
Normal 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)
|
||||
15
common/djangoapps/service_status/urls.py
Normal file
15
common/djangoapps/service_status/urls.py
Normal 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'),
|
||||
)
|
||||
59
common/djangoapps/service_status/views.py
Normal file
59
common/djangoapps/service_status/views.py
Normal 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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
|
||||
1
common/lib/.gitignore
vendored
1
common/lib/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*/jasmine_test_runner.html
|
||||
12
common/lib/calc/setup.py
Normal file
12
common/lib/calc/setup.py
Normal 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"
|
||||
],
|
||||
)
|
||||
@@ -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 = {"'": "'", """: '"'}
|
||||
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('<', '<')
|
||||
raise responsetypes.LoncapaProblemError(msg)
|
||||
finally:
|
||||
sys.path = original_path
|
||||
|
||||
# store code source in context
|
||||
context['script_code'] = all_code
|
||||
return context
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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('<','<'))
|
||||
#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']
|
||||
|
||||
51
common/lib/capa/capa/safe_exec/README.rst
Normal file
51
common/lib/capa/capa/safe_exec/README.rst
Normal 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.
|
||||
3
common/lib/capa/capa/safe_exec/__init__.py
Normal file
3
common/lib/capa/capa/safe_exec/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Capa's specialized use of codejail.safe_exec."""
|
||||
|
||||
from .safe_exec import safe_exec, update_hash
|
||||
42
common/lib/capa/capa/safe_exec/lazymod.py
Normal file
42
common/lib/capa/capa/safe_exec/lazymod.py
Normal 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]
|
||||
130
common/lib/capa/capa/safe_exec/safe_exec.py
Normal file
130
common/lib/capa/capa/safe_exec/safe_exec.py
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
THE_CONST = 23
|
||||
44
common/lib/capa/capa/safe_exec/tests/test_lazymod.py
Normal file
44
common/lib/capa/capa/safe_exec/tests/test_lazymod.py
Normal 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\"')
|
||||
281
common/lib/capa/capa/safe_exec/tests/test_safe_exec.py
Normal file
281
common/lib/capa/capa/safe_exec/tests/test_safe_exec.py
Normal 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)
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
480
common/lib/capa/capa/tests/test_files/snuggletex_correct.html
Normal file
480
common/lib/capa/capa/tests/test_files/snuggletex_correct.html
Normal 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 & 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 & 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"><math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mrow>
|
||||
<mrow>
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mo>&ApplyFunction;</mo>
|
||||
<mfenced close=")" open="(">
|
||||
<mi>theta</mi>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
<mo>&sdot;</mo>
|
||||
<mfenced close="]" open="[">
|
||||
<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>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mrow>
|
||||
<mi>i</mi>
|
||||
<mo>&sdot;</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mo>&ApplyFunction;</mo>
|
||||
<mfenced close=")" open="(">
|
||||
<mi>theta</mi>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
<mo>&sdot;</mo>
|
||||
<mfenced close="]" open="[">
|
||||
<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>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
</mrow>
|
||||
</math></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"><math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<apply>
|
||||
<plus/>
|
||||
<apply>
|
||||
<times/>
|
||||
<apply>
|
||||
<cos/>
|
||||
<ci>theta</ci>
|
||||
</apply>
|
||||
<list>
|
||||
<matrix>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
</matrix>
|
||||
</list>
|
||||
</apply>
|
||||
<apply>
|
||||
<times/>
|
||||
<ci>i</ci>
|
||||
<apply>
|
||||
<sin/>
|
||||
<ci>theta</ci>
|
||||
</apply>
|
||||
<list>
|
||||
<matrix>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
</matrix>
|
||||
</list>
|
||||
</apply>
|
||||
</apply>
|
||||
</math></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><matrix>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
</matrix></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><matrix>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
</matrix></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"><math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<mrow>
|
||||
<mrow>
|
||||
<mi>cos</mi>
|
||||
<mo>&ApplyFunction;</mo>
|
||||
<mfenced close=")" open="(">
|
||||
<mi>theta</mi>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
<mo>&sdot;</mo>
|
||||
<mfenced close="]" open="[">
|
||||
<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>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
<mo>+</mo>
|
||||
<mrow>
|
||||
<mi>i</mi>
|
||||
<mo>&sdot;</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mo>&ApplyFunction;</mo>
|
||||
<mfenced close=")" open="(">
|
||||
<mi>theta</mi>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
<mo>&sdot;</mo>
|
||||
<mfenced close="]" open="[">
|
||||
<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>
|
||||
</mfenced>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<annotation-xml encoding="MathML-Content">
|
||||
<apply>
|
||||
<plus/>
|
||||
<apply>
|
||||
<times/>
|
||||
<apply>
|
||||
<cos/>
|
||||
<ci>theta</ci>
|
||||
</apply>
|
||||
<list>
|
||||
<matrix>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
</matrix>
|
||||
</list>
|
||||
</apply>
|
||||
<apply>
|
||||
<times/>
|
||||
<ci>i</ci>
|
||||
<apply>
|
||||
<sin/>
|
||||
<ci>theta</ci>
|
||||
</apply>
|
||||
<list>
|
||||
<matrix>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
</matrix>
|
||||
</list>
|
||||
</apply>
|
||||
</apply>
|
||||
</annotation-xml>
|
||||
<annotation encoding="ASCIIMathInput"/>
|
||||
<annotation-xml encoding="Maxima-upconversion-failures">
|
||||
<s:fail xmlns:s="http://www.ph.ed.ac.uk/snuggletex" code="UMFG00"
|
||||
message="Content MathML element matrix not supported">
|
||||
<s:arg>matrix</s:arg>
|
||||
<s:xpath>apply[1]/apply[1]/list[1]/matrix[1]</s:xpath>
|
||||
<s:context>
|
||||
<matrix>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
</matrix>
|
||||
</s:context>
|
||||
</s:fail>
|
||||
<s:fail xmlns:s="http://www.ph.ed.ac.uk/snuggletex" code="UMFG00"
|
||||
message="Content MathML element matrix not supported">
|
||||
<s:arg>matrix</s:arg>
|
||||
<s:xpath>apply[1]/apply[2]/list[1]/matrix[1]</s:xpath>
|
||||
<s:context>
|
||||
<matrix>
|
||||
<vector>
|
||||
<cn>0</cn>
|
||||
<cn>1</cn>
|
||||
</vector>
|
||||
<vector>
|
||||
<cn>1</cn>
|
||||
<cn>0</cn>
|
||||
</vector>
|
||||
</matrix>
|
||||
</s:context>
|
||||
</s:fail>
|
||||
</annotation-xml>
|
||||
</semantics>
|
||||
</math></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>
|
||||
187
common/lib/capa/capa/tests/test_files/snuggletex_wrong.html
Normal file
187
common/lib/capa/capa/tests/test_files/snuggletex_wrong.html
Normal 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 & 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 & 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"><math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mn>2</mn>
|
||||
</math></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"><math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<cn>2</cn>
|
||||
</math></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"><math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<semantics>
|
||||
<mn>2</mn>
|
||||
<annotation-xml encoding="MathML-Content">
|
||||
<cn>2</cn>
|
||||
</annotation-xml>
|
||||
<annotation encoding="ASCIIMathInput"/>
|
||||
<annotation encoding="Maxima">2</annotation>
|
||||
</semantics>
|
||||
</math></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>
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</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>⋅</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mrow>
|
||||
<mo>(</mo>
|
||||
<mi>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</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>
|
||||
''',
|
||||
}
|
||||
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>θ</mi><mo>)</mo></mrow>
|
||||
</mrow>
|
||||
<mo>⋅</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>⋅</mo>
|
||||
<mrow>
|
||||
<mi>sin</mi>
|
||||
<mrow>
|
||||
<mo>(</mo><mi>θ</mi><mo>)</mo>
|
||||
</mrow>
|
||||
</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>
|
||||
"""),
|
||||
'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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .calc import evaluator, UndefinedVariable
|
||||
from calc import evaluator, UndefinedVariable
|
||||
from cmath import isinf
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
13
common/lib/capa/symmath/README.md
Normal file
13
common/lib/capa/symmath/README.md
Normal 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
|
||||
2
common/lib/capa/symmath/__init__.py
Normal file
2
common/lib/capa/symmath/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .formula import *
|
||||
from .symmath_check import *
|
||||
739
common/lib/capa/symmath/formula.py
Normal file
739
common/lib/capa/symmath/formula.py
Normal 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>​</mo>
|
||||
<mi>d</mi>
|
||||
</mrow>
|
||||
</msubsup>
|
||||
to be interpreted 'a_b__c'
|
||||
|
||||
also:
|
||||
<msup>
|
||||
<mi>x</mi>
|
||||
<mrow>
|
||||
<mo>​</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>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</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')
|
||||
328
common/lib/capa/symmath/symmath_check.py
Normal file
328
common/lib/capa/symmath/symmath_check.py
Normal 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('<','<'))
|
||||
|
||||
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('<','<')
|
||||
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('<', '<')
|
||||
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('<', '<')
|
||||
msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<', '<')
|
||||
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('<','<')
|
||||
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('<', '<')
|
||||
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('<','<')
|
||||
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('<','<')
|
||||
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
13
common/lib/chem/setup.py
Normal 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",
|
||||
],
|
||||
)
|
||||
1
common/lib/sandbox-packages/README
Normal file
1
common/lib/sandbox-packages/README
Normal file
@@ -0,0 +1 @@
|
||||
This directory is in the Python path for sandboxed Python execution.
|
||||
14
common/lib/sandbox-packages/setup.py
Normal file
14
common/lib/sandbox-packages/setup.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="sandbox-packages",
|
||||
version="0.1",
|
||||
packages=[
|
||||
"verifiers",
|
||||
],
|
||||
py_modules=[
|
||||
"eia",
|
||||
],
|
||||
install_requires=[
|
||||
],
|
||||
)
|
||||
@@ -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 & 1 \\ 1 & 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/>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
66
common/static/coffee/spec/discussion/content_spec.coffee
Normal file
66
common/static/coffee/spec/discussion/content_spec.coffee
Normal 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
|
||||
@@ -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 []
|
||||
@@ -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">–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 []
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
1827
common/static/js/vendor/annotator.js
vendored
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
};
|
||||
|
||||
$.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);
|
||||
2
common/static/js/vendor/annotator.min.js
vendored
Normal file
2
common/static/js/vendor/annotator.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
273
common/static/js/vendor/annotator.store.js
vendored
Normal file
273
common/static/js/vendor/annotator.store.js
vendored
Normal 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);
|
||||
1
common/static/js/vendor/annotator.store.min.js
vendored
Normal file
1
common/static/js/vendor/annotator.store.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
136
common/static/js/vendor/annotator.tags.js
vendored
Normal file
136
common/static/js/vendor/annotator.tags.js
vendored
Normal 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);
|
||||
1
common/static/js/vendor/annotator.tags.min.js
vendored
Normal file
1
common/static/js/vendor/annotator.tags.min.js
vendored
Normal 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);
|
||||
152
common/static/js/vendor/flot/jquery.timeago.js
vendored
Normal file
152
common/static/js/vendor/flot/jquery.timeago.js
vendored
Normal 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));
|
||||
152
common/static/js/vendor/jquery.timeago.js
vendored
Normal file
152
common/static/js/vendor/jquery.timeago.js
vendored
Normal 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));
|
||||
75
common/templates/jasmine/jasmine_test_runner.html.erb
Normal file
75
common/templates/jasmine/jasmine_test_runner.html.erb
Normal 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>
|
||||
|
||||
1
common/test/data/embedded_python/course.xml
Normal file
1
common/test/data/embedded_python/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="embedded_python" url_name="2013_Spring"/>
|
||||
111
common/test/data/embedded_python/course/2013_Spring.xml
Normal file
111
common/test/data/embedded_python/course/2013_Spring.xml
Normal 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="{"tran":[["Z",0.0000004,0.0000009,0.0000014,0.0000019,0.0000024,0.0000029,0.0000034,0.000039]]}"
|
||||
initial_value="[["w",[112,96,128,96]],["w",[256,96,240,96]],["w",[192,96,240,96]],["s",[240,96,0],{"color":"cyan","offset":"","plot offset":"0","_json_":3},["Z"]],["w",[32,224,192,224]],["w",[96,48,192,48]],["L",[256,96,3],{"label":"Z","_json_":6},["Z"]],["r",[192,48,0],{"name":"Rpullup","r":"10K","_json_":7},["1","Z"]],["w",[32,144,32,192]],["w",[32,224,32,192]],["w",[48,192,32,192]],["w",[32,96,32,144]],["w",[48,144,32,144]],["w",[32,48,32,96]],["w",[48,96,32,96]],["w",[32,48,48,48]],["g",[32,224,0],{"_json_":16},["0"]],["v",[96,192,1],{"name":"VC","value":"square(3,0,250K)","_json_":17},["C","0"]],["v",[96,144,1],{"name":"VB","value":"square(3,0,500K)","_json_":18},["B","0"]],["v",[96,96,1],{"name":"VA","value":"square(3,0,1000K)","_json_":19},["A","0"]],["v",[96,48,1],{"name":"Vpwr","value":"dc(3)","_json_":20},["1","0"]],["L",[96,96,2],{"label":"A","_json_":21},["A"]],["w",[96,96,104,96]],["L",[96,144,2],{"label":"B","_json_":23},["B"]],["w",[96,144,104,144]],["L",[96,192,2],{"label":"C","_json_":25},["C"]],["w",[96,192,104,192]],["w",[192,96,192,112]],["s",[112,96,0],{"color":"red","offset":"15","plot offset":"0","_json_":28},["A"]],["w",[104,96,112,96]],["s",[112,144,0],{"color":"green","offset":"10","plot offset":"0","_json_":30},["B"]],["w",[104,144,112,144]],["w",[128,144,112,144]],["s",[112,192,0],{"color":"blue","offset":"5","plot offset":"0","_json_":33},["C"]],["w",[104,192,112,192]],["w",[128,192,112,192]],["view",0,0,2,"5","10","10MEG",null,"100","4us"]]"
|
||||
/>
|
||||
</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) < 2.7: okay = False;
|
||||
if get_value(0.0000009,output) < 2.7: okay = False;
|
||||
if get_value(0.0000014,output) < 2.7: okay = False;
|
||||
if get_value(0.0000019,output) < 2.7: okay = False;
|
||||
if get_value(0.0000024,output) < 2.7: okay = False;
|
||||
if get_value(0.0000029,output) > 0.25: okay = False;
|
||||
if get_value(0.0000034,output) > 0.25: okay = False;
|
||||
if get_value(0.0000039,output) > 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 <= 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>
|
||||
1
common/test/data/embedded_python/roots/2013_Spring.xml
Normal file
1
common/test/data/embedded_python/roots/2013_Spring.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="embedded_python" url_name="2013_Spring"/>
|
||||
@@ -19,7 +19,7 @@ from symmath import *
|
||||
<text>Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 & 1 \\ 1 & 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/>
|
||||
|
||||
Reference in New Issue
Block a user