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

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

6
.gitignore vendored
View File

@@ -9,7 +9,7 @@
:2e#
.AppleDouble
database.sqlite
private-requirements.txt
requirements/private.txt
courseware/static/js/mathjax/*
flushdb.sh
build
@@ -36,3 +36,7 @@ chromedriver.log
/nbproject
ghostdriver.log
node_modules
.pip_download_cache/
.prereqs_cache
autodeploy.properties
.ws_migrations_complete

74
AUTHORS Normal file
View File

@@ -0,0 +1,74 @@
Piotr Mitros <pmitros@edx.org>
Kyle Fiedler <kyle@kylefiedler.com>
Ernie Park <eipark@mit.edu>
Bridger Maxwell <bridger@mit.edu>
Lyla Fischer <lyla@edx.org>
David Ormsbee <dave@edx.org>
Chris Terman <cjt@edx.org>
Reda Lemeden <reda@thoughtbot.com>
Anant Agarwal <agarwal@edx.org>
Jean-Michel Claus <jmc@edx.org>
Calen Pennington <calen.pennington@gmail.com>
JM Van Thong <jm@edx.org>
Prem Sichanugrist <psichanugrist@thoughtbot.com>
Isaac Chuang <ichuang@mit.edu>
Galen Frechette <galen@thoughtbot.com>
Edward Loveall <edward@edwardloveall.com>
Matt Jankowski <mjankowski@thoughtbot.com>
John Jarvis <jarv@edx.org>
Victor Shnayder <victor@edx.org>
Matthew Mongeau <halogenandtoast@gmail.com>
Tony Kim <kimth@edx.org>
Arjun Singh <arjun810@gmail.com>
John Hess <mgojohn@gmail.com>
Carlos Andrés Rocha <rocha@edx.org>
Mike Chen <ccp0101@gmail.com>
Rocky Duan <dementrock@gmail.com>
Sidhanth Rao <sidhanth@mitx.mit.edu>
Brittany Cheng <bcheng42@gmail.com>
Dhaval Adjodah <dhaval@mit.edu>
Tom Giannattasio <tom@mitx.mit.edu>
Ibrahim Awwal <ibrahim.awwal@gmail.com>
Sarina Canelake <sarina@edx.org>
Mark L. Chang <mark.chang@gmail.com>
Dean Dieker <ddieker@gmail.com>
Tommy MacWilliam <tmacwilliam@cs.harvard.edu>
Nate Hardison <natehardison@gmail.com>
Chris Dodge <cdodge@edx.org>
Kevin Chugh <kevinchugh@edx.org>
Ned Batchelder <ned@nedbatchelder.com>
Alexander Kryklia <kryklia@gmail.com>
Vik Paruchuri <vik@edx.org>
Louis Sobel <sobel@edx.org>
Brian Wilson <brian@edx.org>
Ashley Penney <apenney@edx.org>
Don Mitchell <dmitchell@edx.org>
Aaron Culich <aculich@edx.org>
Brian Talbot <btalbot@edx.org>
Jay Zoldak <jzoldak@edx.org>
Valera Rozuvan <valera.rozuvan@gmail.com>
Diana Huang <dkh@edx.org>
Marco Morales <marcotuts@gmail.com>
Christina Roberts <christina@edx.org>
Robert Chirwa <robert@edx.org>
Ed Zarecor <ed@edx.org>
Deena Wang <thedeenawang@gmail.com>
Jean Manuel-Nater <jnater@edx.org>
Emily Zhang <1800.ehz.hang@gmail.com>
Jennifer Akana <jaakana@gmail.com>
Peter Baratta <peter.baratta@gmail.com>
Julian Arni <julian@edx.org>
Arthur Barrett <abarrett@edx.org>
Vasyl Nakvasiuk <vaxxxa@gmail.com>
Will Daly <will@edx.org>
James Tauber <jtauber@jtauber.com>
Greg Price <gprice@edx.org>
Joe Blaylock <jrbl@stanford.edu>
Sef Kloninger <sef@kloninger.com>
Anto Stupak <s2pak.anton@gmail.com>
David Adams <dcadams@stanford.edu>
Steve Strassmann <straz@edx.org>
Giulio Gratta <giulio@giuliogratta.com>
David Baumgold <david@davidbaumgold.com>
Jason Bau <jbau@stanford.edu>
Frances Botsford <frances@edx.org>

View File

@@ -8,7 +8,7 @@ Installation
The installation process is a bit messy at the moment. Here's a high-level
overview of what you should do to get started.
**TLDR:** There is a `create-dev-env.sh` script that will attempt to set all
**TLDR:** There is a `scripts/create-dev-env.sh` script that will attempt to set all
of this up for you. If you're in a hurry, run that script. Otherwise, I suggest
that you understand what the script is doing, and why, by reading this document.
@@ -77,11 +77,16 @@ environment), and Node has a library installer called
Once you've got your languages and virtual environments set up, install
the libraries like so:
$ pip install -r pre-requirements.txt
$ pip install -r requirements.txt
$ pip install -r requirements/edx/base.txt
$ pip install -r requirements/edx/post.txt
$ bundle install
$ npm install
You can also use [`rake`](http://rake.rubyforge.org/) to get all of the prerequisites (or to update)
them if they've changed
$ rake install_prereqs
Other Dependencies
------------------
You'll also need to install [MongoDB](http://www.mongodb.org/), since our
@@ -137,7 +142,7 @@ Studio, visit `127.0.0.1:8001` in your web browser; to view the LMS, visit
There's also an older version of the LMS that saves its information in XML files
in the `data` directory, instead of in Mongo. To run this older version, run:
$ rake lms
$ rake lms
Further Documentation
=====================

View File

@@ -11,7 +11,8 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot click notification "Cancel"
@skip
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
@@ -20,7 +21,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is unchanged
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot click notification "Save"
@skip
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save
@@ -28,7 +30,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is changed
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value
@@ -36,7 +39,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then it is displayed as formatted
@skip-phantom
# Skipped because Ubuntu ChromeDriver cannot edit CodeMirror input
@skip
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes

View File

@@ -10,8 +10,6 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist
And They are correctly selected after I reload the page
@skip-phantom
@skip-firefox
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
@@ -19,8 +17,6 @@ Feature: Course checklists
And I press the browser back button
Then I am brought back to the course outline in the correct state
@skip-phantom
@skip-firefox
Scenario: A task can link to a location outside Studio
Given I have opened Checklists
When I select a link to help page

View File

@@ -1,20 +1,17 @@
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
@skip-phantom
Scenario: User can set course dates
Given I have opened a new course in Studio
When I select Schedule and Details
And I set course dates
Then I see the set dates on refresh
@skip-phantom
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
Then I see cleared dates on refresh
@skip-phantom
Scenario: User cannot clear the course start date
Given I have set course dates
And I clear the course start date

View File

@@ -3,7 +3,6 @@ Feature: Create Section
As a course author
I want to create and edit sections
@skip-phantom
Scenario: Add a new section to a course
Given I have opened a new course in Studio
When I click the New Section link
@@ -27,7 +26,8 @@ Feature: Create Section
And I save a new section release date
Then the section release date is updated
@skip-phantom
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete section
Given I have opened a new course in Studio
And I have added a new section

View File

@@ -18,10 +18,7 @@ def i_fill_in_the_registration_form(step):
@step('I press the Create My Account button on the registration form$')
def i_press_the_button_on_the_registration_form(step):
submit_css = 'form#register_form button#submit'
# Workaround for click not working on ubuntu
# for some unknown reason.
e = world.css_find(submit_css)
e.type(' ')
world.css_click(submit_css)
@step('I should see be on the studio home page$')

View File

@@ -14,7 +14,6 @@ Feature: Overview Toggle Section
When I navigate to the course overview page
Then I do not see the "Collapse All Sections" link
@skip-phantom
Scenario: Collapse link appears after creating first section of a course
Given I have a course with no sections
When I navigate to the course overview page
@@ -22,7 +21,8 @@ Feature: Overview Toggle Section
Then I see the "Collapse All Sections" link
And all sections are expanded
@skip-phantom
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Collapse link is not removed after last section of a course is deleted
Given I have a course with 1 section
And I navigate to the course overview page

View File

@@ -3,14 +3,12 @@ Feature: Create Subsection
As a course author
I want to create and edit subsections
@skip-phantom
Scenario: Add a new subsection to a section
Given I have opened a new course section in Studio
When I click the New Subsection link
And I enter the subsection name and click save
Then I see my subsection on the Courseware page
@skip-phantom
Scenario: Add a new subsection (with a name containing a quote) to a section (bug #216)
Given I have opened a new course section in Studio
When I click the New Subsection link
@@ -27,7 +25,6 @@ Feature: Create Subsection
And I reload the page
Then I see it marked as Homework
@skip-phantom
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I have set a release date and due date in different years
@@ -35,7 +32,8 @@ Feature: Create Subsection
And I reload the page
Then I see the correct dates
@skip-phantom
# Skipped because Ubuntu ChromeDriver hangs on alert
@skip
Scenario: Delete a subsection
Given I have opened a new course section in Studio
And I have added a new subsection

View File

@@ -63,14 +63,6 @@ def test_have_set_dates_in_different_years(step):
set_date_and_time('input#due_date', '01/02/2012', 'input#due_time', '04:00')
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', world.css_find('input#start_date').first.value)
assert_equal('03:00', world.css_find('input#start_time').first.value)
assert_equal('01/02/2012', world.css_find('input#due_date').first.value)
assert_equal('04:00', world.css_find('input#due_time').first.value)
@step('I mark it as Homework$')
def i_mark_it_as_homework(step):
world.css_click('a.menu-toggle')
@@ -101,8 +93,20 @@ def the_subsection_does_not_exist(step):
assert world.browser.is_element_not_present_by_css(css)
@step('I see the correct dates$')
def i_see_the_correct_dates(step):
assert_equal('12/25/2011', get_date('input#start_date'))
assert_equal('03:00', get_date('input#start_time'))
assert_equal('01/02/2012', get_date('input#due_date'))
assert_equal('04:00', get_date('input#due_time'))
############ HELPER METHODS ###################
def get_date(css):
return world.css_find(css).first.value.strip()
def save_subsection_name(name):
name_css = 'input.new-subsection-name-input'
save_css = 'input.new-subsection-name-save'

View File

@@ -74,7 +74,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.client.login(username=uname, password=password)
def check_edit_unit(self, test_course_name):
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
print "Checking ", descriptor.location.url()
@@ -101,7 +101,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
Unfortunately, None = published for the revision field, so get_items() would return
both draft and non-draft copies.
'''
store = modulestore()
store = modulestore('direct')
draft_store = modulestore('draft')
import_from_xml(store, 'common/test/data/', ['simple'])
@@ -128,7 +128,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
module as 'own-metadata' when publishing. Also verifies the metadata inheritance is
properly computed
'''
store = modulestore()
store = modulestore('direct')
draft_store = modulestore('draft')
import_from_xml(store, 'common/test/data/', ['simple'])
@@ -186,7 +186,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(html_module.lms.graceperiod, new_graceperiod)
def test_get_depth_with_drafts(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
course = modulestore('draft').get_item(
Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None]),
@@ -221,17 +221,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(num_drafts, 1)
def test_import_textbook_as_content_element(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
self.assertGreater(len(course.textbooks), 0)
def test_static_tab_reordering(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
# reverse the ordering
@@ -253,10 +253,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(reverse_tabs, course_tabs)
def test_import_polls(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
found = False
import_from_xml(module_store, 'common/test/data/', ['full'])
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
found = len(items) > 0
@@ -270,9 +268,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertGreater(err_cnt, 0)
def test_delete(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
direct_store = modulestore('direct')
import_from_xml(direct_store, 'common/test/data/', ['full'])
sequential = direct_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
@@ -306,8 +303,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
while there is a base definition in /about/effort.html
'''
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
effort = module_store.get_item(Location(['i4x', 'edX', 'full', 'about', 'effort', None]))
self.assertEqual(effort.data, '6 hours')
@@ -316,9 +314,8 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(effort.data, 'TBD')
def test_remove_hide_progress_tab(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
@@ -333,14 +330,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'display_name': 'Robot Super Course',
}
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
resp = self.client.post(reverse('create_new_course'), course_data)
self.assertEqual(resp.status_code, 200)
data = parse_json(resp)
self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
module_store = modulestore('direct')
content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
@@ -365,9 +362,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 400)
def test_delete_course(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
content_store = contentstore()
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
@@ -523,8 +520,9 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_prefetch_children(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
@@ -736,7 +734,7 @@ class ContentStoreTest(ModuleStoreTestCase):
Import and walk through some common URL endpoints. This just verifies non-500 and no other
correct behavior, so it is not a deep test
"""
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
import_from_xml(modulestore('direct'), 'common/test/data/', ['simple'])
loc = Location(['i4x', 'edX', 'simple', 'course', '2012_Fall', None])
resp = self.client.get(reverse('course_index',
kwargs={'org': loc.org,
@@ -838,9 +836,11 @@ class ContentStoreTest(ModuleStoreTestCase):
json.dumps({'id': del_loc.url()}), "application/json")
self.assertEqual(200, resp.status_code)
def test_import_metadata_with_attempts_empty_string(self):
import_from_xml(modulestore(), 'common/test/data/', ['simple'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['simple'])
did_load_item = False
try:
module_store.get_item(Location(['i4x', 'edX', 'simple', 'problem', 'ps01-simple', None]))
@@ -852,8 +852,9 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertTrue(did_load_item)
def test_forum_id_generation(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
new_component_location = Location('i4x', 'edX', 'full', 'discussion', 'new_component')
source_template_location = Location('i4x', 'edx', 'templates', 'discussion', 'Discussion_Tag')
@@ -865,9 +866,8 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
def test_update_modulestore_signal_did_fire(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
try:
module_store.modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
@@ -891,9 +891,9 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertTrue(self.got_signal)
def test_metadata_inheritance(self):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['full'])
course = module_store.get_item(Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None]))
verticals = module_store.get_items(['i4x', 'edX', 'full', 'vertical', None, None])

View File

@@ -17,7 +17,6 @@ from xmodule.modulestore.tests.factories import CourseFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.fields import Date
@@ -256,7 +255,7 @@ class CourseMetadataEditingTest(CourseTestCase):
def setUp(self):
CourseTestCase.setUp(self)
# add in the full class too
import_from_xml(modulestore(), 'common/test/data/', ['full'])
import_from_xml(get_modulestore(self.course_location), 'common/test/data/', ['full'])
self.fullcourse_location = Location(['i4x', 'edX', 'full', 'course', '6.002_Spring_2012', None])
def test_fetch_initial_fields(self):

View File

@@ -0,0 +1,29 @@
from contentstore.utils import get_modulestore, get_url_reverse
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from django.core.urlresolvers import reverse
class DeleteItem(CourseTestCase):
def setUp(self):
""" Creates the test course with a static page in it. """
super(DeleteItem, self).setUp()
self.course = CourseFactory.create(org='mitX', number='333', display_name='Dummy Course')
def testDeleteStaticPage(self):
# Add static tab
data = {
'parent_location': 'i4x://mitX/333/course/Dummy_Course',
'template': 'i4x://edx/templates/static_tab/Empty'
}
resp = self.client.post(reverse('clone_item'), data)
self.assertEqual(resp.status_code, 200)
# Now delete it. There was a bug that the delete was failing (static tabs do not exist in draft modulestore).
resp = self.client.post(reverse('delete_item'), resp.content, "application/json")
self.assertEqual(resp.status_code, 200)

View File

@@ -1,6 +1,8 @@
""" Tests for utils. """
from contentstore import utils
import mock
import collections
import copy
from django.test import TestCase
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -70,3 +72,79 @@ class UrlReverseTestCase(ModuleStoreTestCase):
'https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about',
utils.get_url_reverse('https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about', course)
)
class ExtraPanelTabTestCase(TestCase):
""" Tests adding and removing extra course tabs. """
def get_tab_type_dicts(self, tab_types):
""" Returns an array of tab dictionaries. """
if tab_types:
return [{'tab_type': tab_type} for tab_type in tab_types.split(',')]
else:
return []
def get_course_with_tabs(self, tabs=[]):
""" Returns a mock course object with a tabs attribute. """
course = collections.namedtuple('MockCourse', ['tabs'])
if isinstance(tabs, basestring):
course.tabs = self.get_tab_type_dicts(tabs)
else:
course.tabs = tabs
return course
def test_add_extra_panel_tab(self):
""" Tests if a tab can be added to a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test adding with changed = True
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
expected_tabs.append(tab)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test adding with changed = False
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.add_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)
def test_remove_extra_panel_tab(self):
""" Tests if a tab can be removed from a course tab list. """
for tab_type in utils.EXTRA_TAB_PANELS.keys():
tab = utils.EXTRA_TAB_PANELS.get(tab_type)
# test removing with changed = True
tab_test_setup = [
[tab],
[tab, self.get_tab_type_dicts('x,y,z')],
[self.get_tab_type_dicts('x,y'), tab, self.get_tab_type_dicts('z')],
[self.get_tab_type_dicts('x,y,z'), tab]]
for tab_setup in tab_test_setup:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = [t for t in course.tabs if t != utils.EXTRA_TAB_PANELS.get(tab_type)]
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertTrue(changed)
self.assertEqual(actual_tabs, expected_tabs)
# test removing with changed = False
for tab_setup in ['', 'x', 'x,y,z']:
course = self.get_course_with_tabs(tab_setup)
expected_tabs = copy.copy(course.tabs)
changed, actual_tabs = utils.remove_extra_panel_tab(tab_type, course)
self.assertFalse(changed)
self.assertEqual(actual_tabs, expected_tabs)

View File

@@ -9,6 +9,8 @@ DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_ta
#In order to instantiate an open ended tab automatically, need to have this data
OPEN_ENDED_PANEL = {"name": "Open Ended Panel", "type": "open_ended"}
NOTES_PANEL = {"name": "My Notes", "type": "notes"}
EXTRA_TAB_PANELS = dict([(p['type'], p) for p in [OPEN_ENDED_PANEL, NOTES_PANEL]])
def get_modulestore(location):
@@ -192,9 +194,10 @@ class CoursePageNames:
Checklists = "checklists"
def add_open_ended_panel_tab(course):
def add_extra_panel_tab(tab_type, course):
"""
Used to add the open ended panel tab to a course if it does not exist.
Used to add the panel tab to a course if it does not exist.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -202,16 +205,19 @@ def add_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL not in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel not in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs.append(OPEN_ENDED_PANEL)
course_tabs.append(tab_panel)
changed = True
return changed, course_tabs
def remove_open_ended_panel_tab(course):
def remove_extra_panel_tab(tab_type, course):
"""
Used to remove the open ended panel tab from a course if it exists.
Used to remove the panel tab from a course if it exists.
@param tab_type: A string representing the tab type.
@param course: A course object from the modulestore.
@return: Boolean indicating whether or not a tab was added and a list of tabs for the course.
"""
@@ -219,8 +225,10 @@ def remove_open_ended_panel_tab(course):
course_tabs = copy.copy(course.tabs)
changed = False
#Check to see if open ended panel is defined in the course
if OPEN_ENDED_PANEL in course_tabs:
tab_panel = EXTRA_TAB_PANELS.get(tab_type)
if tab_panel in course_tabs:
#Add panel to the tabs if it is not defined
course_tabs = [ct for ct in course_tabs if ct != OPEN_ENDED_PANEL]
course_tabs = [ct for ct in course_tabs if ct != tab_panel]
changed = True
return changed, course_tabs

View File

@@ -41,7 +41,8 @@ log = logging.getLogger(__name__)
COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES
NOTE_COMPONENT_TYPES = ['notes']
ADVANCED_COMPONENT_TYPES = ['annotatable' + 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
ADVANCED_COMPONENT_CATEGORY = 'advanced'
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'

View File

@@ -20,8 +20,8 @@ from xmodule.modulestore import Location
from contentstore.course_info_model \
import get_course_updates, update_course_updates, delete_course_update
from contentstore.utils \
import get_lms_link_for_item, add_open_ended_panel_tab, \
remove_open_ended_panel_tab
import get_lms_link_for_item, add_extra_panel_tab, \
remove_extra_panel_tab
from models.settings.course_details \
import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel
@@ -32,7 +32,8 @@ from util.json_request import expect_json
from .access import has_access, get_location_and_verify_access
from .requests import get_request_method
from .tabs import initialize_course_tabs
from .component import OPEN_ENDED_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
from .component import OPEN_ENDED_COMPONENT_TYPES, \
NOTE_COMPONENT_TYPES, ADVANCED_COMPONENT_POLICY_KEY
__all__ = ['course_index', 'create_new_course', 'course_info',
'course_info_updates', 'get_course_settings',
@@ -352,38 +353,52 @@ def course_advanced_updates(request, org, course, name):
request_body = json.loads(request.body)
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = True
# Check to see if the user instantiated any advanced components.
# This is a hack to add the open ended panel tab
# to a course automatically if the user has indicated that they want
# to edit the combinedopenended or peergrading
# module, and to remove it if they have removed the open ended elements.
#Check to see if the user instantiated any advanced components. This is a hack
#that does the following :
# 1) adds/removes the open ended panel tab to a course automatically if the user
# has indicated that they want to edit the combinedopendended or peergrading module
# 2) adds/removes the notes panel tab to a course automatically if the user has
# indicated that they want the notes module enabled in their course
# TODO refactor the above into distinct advanced policy settings
if ADVANCED_COMPONENT_POLICY_KEY in request_body:
# Check to see if the user instantiated any open ended components
found_oe_type = False
# Get the course so that we can scrape current tabs
#Get the course so that we can scrape current tabs
course_module = modulestore().get_item(location)
for oe_type in OPEN_ENDED_COMPONENT_TYPES:
if oe_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
# Add an open ended tab to the course if needed
changed, new_tabs = add_open_ended_panel_tab(course_module)
# If a tab has been added to the course, then send the
# metadata along to CourseMetadata.update_from_json
#Maps tab types to components
tab_component_map = {
'open_ended': OPEN_ENDED_COMPONENT_TYPES,
'notes': NOTE_COMPONENT_TYPES,
}
#Check to see if the user instantiated any notes or open ended components
for tab_type in tab_component_map.keys():
component_types = tab_component_map.get(tab_type)
found_ac_type = False
for ac_type in component_types:
if ac_type in request_body[ADVANCED_COMPONENT_POLICY_KEY]:
#Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
#If a tab has been added to the course, then send the metadata along to CourseMetadata.update_from_json
if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
#Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
#Set this flag to avoid the tab removal code below.
found_ac_type = True
break
#If we did not find a module type in the advanced settings,
# we may need to remove the tab from the course.
if not found_ac_type:
#Remove tab from the course if needed
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed:
course_module.tabs = new_tabs
request_body.update({'tabs': new_tabs})
# Indicate that tabs should not be filtered out of the metadata
#Indicate that tabs should *not* be filtered out of the metadata
filter_tabs = False
# Set this flag to avoid the open ended tab removal code below.
found_oe_type = True
break
# If we did not find an open ended module type in the advanced settings,
# we may need to remove the open ended tab from the course.
if not found_oe_type:
# Remove open ended tab to the course if needed
changed, new_tabs = remove_open_ended_panel_tab(course_module)
if changed:
request_body.update({'tabs': new_tabs})
# Indicate that tabs should not be filtered out of the metadata
filter_tabs = False
response_json = json.dumps(CourseMetadata.update_from_json(location,
request_body,
filter_tabs=filter_tabs))

View File

@@ -113,7 +113,7 @@ def delete_item(request):
delete_children = request.POST.get('delete_children', False)
delete_all_versions = request.POST.get('delete_all_versions', False)
store = modulestore()
store = get_modulestore(item_location)
item = store.get_item(item_location)

View File

@@ -8,27 +8,41 @@ from .test import *
# otherwise the browser will not render the pages correctly
DEBUG = True
# Show the courses that are in the data directory
COURSES_ROOT = ENV_ROOT / "data"
DATA_DIR = COURSES_ROOT
# MODULESTORE = {
# 'default': {
# 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
# 'OPTIONS': {
# 'data_dir': DATA_DIR,
# 'default_class': 'xmodule.hidden_module.HiddenDescriptor',
# }
# }
# }
# Disable warnings for acceptance tests, to make the logs readable
import logging
logging.disable(logging.ERROR)
MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'acceptance_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'draft': {
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
}
}
# Set this up so that rake lms[acceptance] and running the
# harvest command both use the same (test) database
# which they can flush without messing up your dev db
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "test_mitx.db",
'TEST_NAME': ENV_ROOT / "db" / "test_mitx.db",
'NAME': TEST_ROOT / "db" / "test_mitx.db",
'TEST_NAME': TEST_ROOT / "db" / "test_mitx.db",
}
}

View File

@@ -28,6 +28,48 @@ EMAIL_BACKEND = 'django_ses.SESBackend'
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
###################################### CELERY ################################
# Don't use a connection pool, since connections are dropped by ELB.
BROKER_POOL_LIMIT = 0
BROKER_CONNECTION_TIMEOUT = 1
# For the Result Store, use the django cache named 'celery'
CELERY_RESULT_BACKEND = 'cache'
CELERY_CACHE_BACKEND = 'celery'
# When the broker is behind an ELB, use a heartbeat to refresh the
# connection and to detect if it has been dropped.
BROKER_HEARTBEAT = 10.0
BROKER_HEARTBEAT_CHECKRATE = 2
# Each worker should only fetch one message at a time
CELERYD_PREFETCH_MULTIPLIER = 1
# Skip djcelery migrations, since we don't use the database as the broker
SOUTH_MIGRATION_MODULES = {
'djcelery': 'ignore',
}
# Rename the exchange and queues for each variant
QUEUE_VARIANT = CONFIG_PREFIX.lower()
CELERY_DEFAULT_EXCHANGE = 'edx.{0}core'.format(QUEUE_VARIANT)
HIGH_PRIORITY_QUEUE = 'edx.{0}core.high'.format(QUEUE_VARIANT)
DEFAULT_PRIORITY_QUEUE = 'edx.{0}core.default'.format(QUEUE_VARIANT)
LOW_PRIORITY_QUEUE = 'edx.{0}core.low'.format(QUEUE_VARIANT)
CELERY_DEFAULT_QUEUE = DEFAULT_PRIORITY_QUEUE
CELERY_DEFAULT_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE
CELERY_QUEUES = {
HIGH_PRIORITY_QUEUE: {},
LOW_PRIORITY_QUEUE: {},
DEFAULT_PRIORITY_QUEUE: {}
}
############# NON-SECURE ENV CONFIG ##############################
# Things like server locations, ports, etc.
with open(ENV_ROOT / CONFIG_PREFIX + "env.json") as env_file:
@@ -78,3 +120,14 @@ CONTENTSTORE = AUTH_TOKENS['CONTENTSTORE']
# Datadog for events!
DATADOG_API = AUTH_TOKENS.get("DATADOG_API")
# Celery Broker
CELERY_BROKER_TRANSPORT = ENV_TOKENS.get("CELERY_BROKER_TRANSPORT", "")
CELERY_BROKER_HOSTNAME = ENV_TOKENS.get("CELERY_BROKER_HOSTNAME", "")
CELERY_BROKER_USER = AUTH_TOKENS.get("CELERY_BROKER_USER", "")
CELERY_BROKER_PASSWORD = AUTH_TOKENS.get("CELERY_BROKER_PASSWORD", "")
BROKER_URL = "{0}://{1}:{2}@{3}".format(CELERY_BROKER_TRANSPORT,
CELERY_BROKER_USER,
CELERY_BROKER_PASSWORD,
CELERY_BROKER_HOSTNAME)

View File

@@ -34,6 +34,9 @@ MITX_FEATURES = {
'STAFF_EMAIL': '', # email address for staff (eg to request course creation)
'STUDIO_NPS_SURVEY': True,
'SEGMENT_IO': True,
# Enable URL that shows information about the status of variuous services
'ENABLE_SERVICE_STATUS': False,
}
ENABLE_JASMINE = False
@@ -240,6 +243,51 @@ STATICFILES_IGNORE_PATTERNS = (
PIPELINE_YUI_BINARY = 'yui-compressor'
################################# CELERY ######################################
# Message configuration
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_MESSAGE_COMPRESSION = 'gzip'
# Results configuration
CELERY_IGNORE_RESULT = False
CELERY_STORE_ERRORS_EVEN_IF_IGNORED = True
# Events configuration
CELERY_TRACK_STARTED = True
CELERY_SEND_EVENTS = True
CELERY_SEND_TASK_SENT_EVENT = True
# Exchange configuration
CELERY_DEFAULT_EXCHANGE = 'edx.core'
CELERY_DEFAULT_EXCHANGE_TYPE = 'direct'
# Queues configuration
HIGH_PRIORITY_QUEUE = 'edx.core.high'
DEFAULT_PRIORITY_QUEUE = 'edx.core.default'
LOW_PRIORITY_QUEUE = 'edx.core.low'
CELERY_QUEUE_HA_POLICY = 'all'
CELERY_CREATE_MISSING_QUEUES = True
CELERY_DEFAULT_QUEUE = DEFAULT_PRIORITY_QUEUE
CELERY_DEFAULT_ROUTING_KEY = DEFAULT_PRIORITY_QUEUE
CELERY_QUEUES = {
HIGH_PRIORITY_QUEUE: {},
LOW_PRIORITY_QUEUE: {},
DEFAULT_PRIORITY_QUEUE: {}
}
############################ APPS #####################################
INSTALLED_APPS = (
@@ -249,8 +297,12 @@ INSTALLED_APPS = (
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'djcelery',
'south',
# Monitor the status of services
'service_status',
# For CMS
'contentstore',
'auth',
@@ -265,3 +317,7 @@ INSTALLED_APPS = (
'staticfiles',
'static_replace',
)
################# EDX MARKETING SITE ##################################
EDXMKTG_COOKIE_NAME = 'edxloggedin'

View File

@@ -116,6 +116,11 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
PIPELINE_SASS_ARGUMENTS = '--debug-info --require {proj_dir}/static/sass/bourbon/lib/bourbon.rb'.format(proj_dir=PROJECT_ROOT)
################################# CELERY ######################################
# By default don't use a worker, execute tasks as if they were local functions
CELERY_ALWAYS_EAGER = True
################################ DEBUG TOOLBAR #################################
INSTALLED_APPS += ('debug_toolbar', 'debug_toolbar_mongo')
MIDDLEWARE_CLASSES += ('django_comment_client.utils.QueryCountDebugMiddleware',
@@ -151,5 +156,8 @@ DEBUG_TOOLBAR_MONGO_STACKTRACES = True
# disable NPS survey in dev mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
# Enable URL that shows information about the status of variuous services
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True
# segment-io key for dev
SEGMENT_IO_KEY = 'mty8edrrsg'

View File

@@ -0,0 +1,35 @@
"""
This config file follows the dev enviroment, but adds the
requirement of a celery worker running in the background to process
celery tasks.
The worker can be executed using:
django_admin.py celery worker
"""
from dev import *
################################# CELERY ######################################
# Requires a separate celery worker
CELERY_ALWAYS_EAGER = False
# Use django db as the broker and result store
BROKER_URL = 'django://'
INSTALLED_APPS += ('djcelery.transport', )
CELERY_RESULT_BACKEND = 'database'
DJKOMBU_POLLING_INTERVAL = 1.0
# Disable transaction management because we are using a worker. Views
# that request a task and wait for the result will deadlock otherwise.
MIDDLEWARE_CLASSES = tuple(
c for c in MIDDLEWARE_CLASSES
if c != 'django.middleware.transaction.TransactionMiddleware')
# Note: other alternatives for disabling transactions don't work in 1.4
# https://code.djangoproject.com/ticket/2304
# https://code.djangoproject.com/ticket/16039

View File

@@ -41,14 +41,14 @@ MODULESTORE_OPTIONS = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
'collection': 'test_modulestore',
'fs_root': TEST_ROOT / "data",
'render_template': 'mitxmako.shortcuts.render_to_string',
}
MODULESTORE = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'OPTIONS': MODULESTORE_OPTIONS
},
'direct': {
@@ -108,6 +108,12 @@ CACHES = {
}
}
################################# CELERY ######################################
CELERY_ALWAYS_EAGER = True
CELERY_RESULT_BACKEND = 'cache'
BROKER_TRANSPORT = 'memory'
################### Make tests faster
#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
PASSWORD_HASHERS = (
@@ -121,3 +127,4 @@ SEGMENT_IO_KEY = '***REMOVED***'
# disable NPS survey in test mode
MITX_FEATURES['STUDIO_NPS_SURVEY'] = False
MITX_FEATURES['ENABLE_SERVICE_STATUS'] = True

View File

@@ -801,7 +801,8 @@ hr.divide {
}
.tooltip {
@extend .t-copy-sub2;
@include font-size(12);
@include transition(opacity 0.1s ease-out);
position: absolute;
top: 0;
left: 0;
@@ -811,10 +812,9 @@ hr.divide {
background: rgba(0, 0, 0, 0.85);
font-weight: normal;
line-height: 26px;
color: #fff;
color: $white;
pointer-events: none;
opacity: 0;
@include transition(opacity 0.1s ease-out);
&:after {
content: '';

View File

@@ -184,6 +184,6 @@ $lightBluishGrey2: rgb(213, 220, 228);
$error-red: rgb(253, 87, 87);
// type
$sans-serif: $f-serif;
$sans-serif: $f-sans-serif;
$body-line-height: golden-ratio(.875em, 1);

View File

@@ -1,8 +1,49 @@
// studio - assets - fonts
// NOTE: Sass currently can't process the standard Google Web Font import method, so a @font-face with src declaration of the .woff file that the Google @import method uses is needed :/
// ====================
// import from google fonts - Open Sans
@import url(http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,700,300);
// Open Sans - http://www.google.com/fonts/specimen/Open+Sans
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSansLight-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format('woff');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
// import from google fonts - Bree
@import url(http://fonts.googleapis.com/css?family=Bree+Serif);
// Bree Serif - http://www.google.com/fonts/specimen/Bree+Serif
@font-face {
font-family: 'Bree Serif';
font-style: normal;
font-weight: 400;
src: local('Bree Serif'), local('BreeSerif'), url(http://themes.googleusercontent.com/static/fonts/breeserif/v2/LQ7WLTaITDg4OSRuOZCps73hpw3pgy2gAi-Ip7WPMi0.woff) format('woff');
}

View File

@@ -106,13 +106,19 @@
width: 1px;
}
.course-org {
margin-right: ($baseline/4);
}
.course-number, .course-org {
@include font-size(12);
display: inline-block;
max-width: 70px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.course-org {
margin-right: ($baseline/4);
max-width: 140px;
}
.course-title {
@@ -132,9 +138,9 @@
// specific elements - course nav
.nav-course {
width: 285px;
width: 290px;
@extend .t-copy-sub1;
margin-top: -($baseline/4);
@include font-size(14);
> ol > .nav-item {
vertical-align: bottom;
@@ -152,8 +158,8 @@
color: $gray-d3;
.label-prefix {
display: block;
@include font-size(11);
display: block;
font-weight: 400;
}
}

View File

@@ -135,6 +135,12 @@ if settings.ENABLE_JASMINE:
# # Jasmine
urlpatterns = urlpatterns + (url(r'^_jasmine/', include('django_jasmine.urls')),)
if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
urlpatterns += (
url(r'^status/', include('service_status.urls')),
)
urlpatterns = patterns(*urlpatterns)
# Custom error pages

1
common/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -736,4 +736,4 @@ def test6(): # imaginary numbers
</mstyle>
</math>
'''
return formula(xmlstr, options='imaginaryi')
return formula(xmlstr, options='imaginary')

View File

@@ -324,4 +324,5 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
msg += "<p>Difference: %s</p>" % to_latex(diff)
msg += '<hr>'
return {'ok': False, 'msg': msg, 'ex': fexpect, 'got': fsym}
# Used to return more keys: 'ex': fexpect, 'got': fsym
return {'ok': False, 'msg': msg}

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More