Commit 3cf91500 by marco

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

Merge branch 'feature/christina/metadata-ui' of github.com:edx/edx-platform into feature/christina/metadata-ui
parents 79c78cbc 4d7c7f86
......@@ -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
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>
......@@ -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
=====================
......
......@@ -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
......
......@@ -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
......
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
......
......@@ -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
......
......@@ -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$')
......
......@@ -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
......
......@@ -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
......
......@@ -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'
......
......@@ -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])
......
......@@ -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):
......
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)
""" 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)
......@@ -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
......@@ -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'
......
......@@ -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))
......
......@@ -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)
......
......@@ -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",
}
}
......
......@@ -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)
......@@ -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'
......@@ -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'
"""
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
......@@ -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
......@@ -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: '▾';
......
......@@ -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);
// 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');
}
......@@ -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;
}
}
......
......@@ -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
......
jasmine_test_runner.html
......@@ -14,9 +14,57 @@
from django.template import Context
from django.http import HttpResponse
import logging
from . import middleware
from django.conf import settings
from django.core.urlresolvers import reverse
log = logging.getLogger(__name__)
def marketing_link(name):
"""Returns the correct URL for a link to the marketing site
depending on if the marketing site is enabled
Since the marketing site is enabled by a setting, we have two
possible URLs for certain links. This function is to decides
which URL should be provided.
"""
# link_map maps URLs from the marketing site to the old equivalent on
# the Django site
link_map = settings.MKTG_URL_LINK_MAP
if settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in settings.MKTG_URLS:
# special case for when we only want the root marketing URL
if name == 'ROOT':
return settings.MKTG_URLS.get('ROOT')
return settings.MKTG_URLS.get('ROOT') + settings.MKTG_URLS.get(name)
# only link to the old pages when the marketing site isn't on
elif not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE') and name in link_map:
return reverse(link_map[name])
else:
log.warning("Cannot find corresponding link for name: {name}".format(name=name))
return '#'
def marketing_link_context_processor(request):
"""
A django context processor to give templates access to marketing URLs
Returns a dict whose keys are the marketing link names usable with the
marketing_link method (e.g. 'ROOT', 'CONTACT', etc.) prefixed with
'MKTG_URL_' and whose values are the corresponding URLs as computed by the
marketing_link method.
"""
return dict(
[
("MKTG_URL_" + k, marketing_link(k))
for k in (
settings.MKTG_URL_LINK_MAP.viewkeys() |
settings.MKTG_URLS.viewkeys()
)
]
)
def render_to_string(template_name, dictionary, context=None, namespace='main'):
......@@ -27,6 +75,7 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_dictionary = {}
context_instance['settings'] = settings
context_instance['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
context_instance['marketing_link'] = marketing_link
# In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None:
......
......@@ -14,6 +14,7 @@
from django.conf import settings
from mako.template import Template as MakoTemplate
from mitxmako.shortcuts import marketing_link
from mitxmako import middleware
......@@ -37,7 +38,6 @@ class Template(MakoTemplate):
kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs)
def render(self, context_instance):
"""
This takes a render call with a context (from Django) and translates
......@@ -55,5 +55,6 @@ class Template(MakoTemplate):
context_dictionary['settings'] = settings
context_dictionary['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
context_dictionary['django_context'] = context_instance
context_dictionary['marketing_link'] = marketing_link
return super(Template, self).render_unicode(**context_dictionary)
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)
"""
Stub for a Django app to report the status of various services
"""
"""
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
"""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)
"""
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'),
)
"""
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")
"""
Browser set up for acceptance tests.
"""
#pylint: disable=E1101
#pylint: disable=W0613
#pylint: disable=W0611
from lettuce import before, after, world
from splinter.browser import Browser
from logging import getLogger
from django.core.management import call_command
from django.conf import settings
from selenium.common.exceptions import WebDriverException
# Let the LMS and CMS do their one-time setup
# For example, setting up mongo caches
from lms import one_time_startup
from cms import one_time_startup
logger = getLogger(__name__)
logger.info("Loading the lettuce acceptance testing terrain file...")
# There is an import issue when using django-staticfiles with lettuce
# Lettuce assumes that we are using django.contrib.staticfiles,
# but the rest of the app assumes we are using django-staticfiles
# (in particular, django-pipeline and our mako implementation)
# To resolve this, we check whether staticfiles is installed,
# then redirect imports for django.contrib.staticfiles
# to use staticfiles.
try:
import staticfiles
except ImportError:
pass
else:
import sys
sys.modules['django.contrib.staticfiles'] = staticfiles
LOGGER = getLogger(__name__)
LOGGER.info("Loading the lettuce acceptance testing terrain file...")
MAX_VALID_BROWSER_ATTEMPTS = 20
@before.harvest
def initial_setup(server):
'''
Launch the browser once before executing the tests
'''
"""
Launch the browser once before executing the tests.
"""
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
world.browser = Browser(browser_driver)
# There is an issue with ChromeDriver2 r195627 on Ubuntu
# in which we sometimes get an invalid browser session.
# This is a work-around to ensure that we get a valid session.
success = False
num_attempts = 0
while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
# Get a browser session
world.browser = Browser(browser_driver)
# Try to visit the main page
# If the browser session is invalid, this will
# raise a WebDriverException
try:
world.visit('/')
except WebDriverException:
world.browser.quit()
num_attempts += 1
else:
success = True
# If we were unable to get a valid session within the limit of attempts,
# then we cannot run the tests.
if not success:
raise IOError("Could not acquire valid ChromeDriver browser session.")
@before.each_scenario
def reset_data(scenario):
'''
"""
Clean out the django test database defined in the
envs/acceptance.py file: mitx_all/db/test_mitx.db
'''
logger.debug("Flushing the test database...")
"""
LOGGER.debug("Flushing the test database...")
call_command('flush', interactive=False)
@after.each_scenario
def screenshot_on_error(scenario):
'''
Save a screenshot to help with debugging
'''
"""
Save a screenshot to help with debugging.
"""
if scenario.failed:
world.browser.driver.save_screenshot('/tmp/last_failed_scenario.png')
@after.all
def teardown_browser(total):
'''
Quit the browser after executing the tests
'''
"""
Quit the browser after executing the tests.
"""
world.browser.quit()
pass
......@@ -38,9 +38,11 @@ def create_user(uname):
@world.absorb
def log_in(username, password):
'''
Log the user in programatically
'''
"""
Log the user in programatically.
This will delete any existing cookies to ensure that the user
logs in to the correct session.
"""
# Authenticate the user
user = authenticate(username=username, password=password)
......@@ -60,15 +62,8 @@ def log_in(username, password):
# Retrieve the sessionid and add it to the browser's cookies
cookie_dict = {settings.SESSION_COOKIE_NAME: request.session.session_key}
try:
world.browser.cookies.add(cookie_dict)
# WebDriver has an issue where we cannot set cookies
# before we make a GET request, so if we get an error,
# we load the '/' page and try again
except:
world.browser.visit(django_url('/'))
world.browser.cookies.add(cookie_dict)
world.browser.cookies.delete()
world.browser.cookies.add(cookie_dict)
@world.absorb
......
......@@ -122,6 +122,13 @@ def should_see_a_link_called(step, text):
assert len(world.browser.find_link_by_text(text)) > 0
@step(r'should see (?:the|a) link with the id "([^"]*)" called "([^"]*)"$')
def should_have_link_with_id_and_text(step, link_id, text):
link = world.browser.find_by_id(link_id)
assert len(link) > 0
assert_equals(link.text, text)
@step(r'should see "(.*)" (?:somewhere|anywhere) in (?:the|this) page')
def should_see_in_the_page(step, text):
assert_in(text, world.css_text('body'))
......@@ -144,3 +151,8 @@ def i_am_an_edx_user(step):
@step(u'User "([^"]*)" is an edX user$')
def registered_edx_user(step, uname):
world.create_user(uname)
@step(u'All dialogs should be closed$')
def dialogs_are_closed(step):
assert world.dialogs_closed()
#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()
......@@ -108,10 +105,21 @@ def css_visible(css_selector):
@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
html = world.browser.html.encode('ascii', 'ignore')
filename = '%s.html' % quote_plus(u)
f = open('%s/%s' % (path, filename), 'w')
f.write(html)
f.close
f.close()
......@@ -16,7 +16,7 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from urllib import urlencode
import zendesk
import capa.calc
import calc
import track.views
......@@ -27,7 +27,7 @@ def calculate(request):
''' Calculator in footer of every page. '''
equation = request.GET['equation']
try:
result = capa.calc.evaluator({}, {}, equation)
result = calc.evaluator({}, {}, equation)
except:
event = {'error': map(str, sys.exc_info()),
'equation': equation}
......
*/jasmine_test_runner.html
from setuptools import setup
setup(
name="calc",
version="0.1",
py_modules=["calc"],
install_requires=[
"pyparsing==1.5.6",
"numpy",
"scipy"
],
)
......@@ -13,33 +13,19 @@ Main module which shows problems (of "capa" type).
This is used by capa_module.
'''
from __future__ import division
from datetime import datetime
import logging
import math
import numpy
import os
import random
import os.path
import re
import scipy
import struct
import sys
from lxml import etree
from xml.sax.saxutils import unescape
from copy import deepcopy
import chem
import chem.miller
import chem.chemcalc
import chem.chemtools
import verifiers
import verifiers.draganddrop
import calc
from .correctmap import CorrectMap
import eia
import inputtypes
import customrender
from .util import contextualize_text, convert_files_to_filenames
......@@ -47,6 +33,7 @@ import xqueue_interface
# to be replaced with auto-registering
import responsetypes
import safe_exec
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
......@@ -63,17 +50,6 @@ html_transforms = {'problem': {'tag': 'div'},
"math": {'tag': 'span'},
}
global_context = {'random': random,
'numpy': numpy,
'math': math,
'scipy': scipy,
'calc': calc,
'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup", "openendedparam", "openendedrubric"]
......@@ -96,7 +72,7 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- seed (int): random number generator seed (int)
- seed (int): random number generator seed (int)
- state (dict): containing the following keys:
- 'seed' - (int) random number generator seed
- 'student_answers' - (dict) maps input id to the stored answer for that input
......@@ -115,23 +91,20 @@ class LoncapaProblem(object):
if self.system is None:
raise Exception()
state = state if state else {}
state = state or {}
# Set seed according to the following priority:
# 1. Contained in problem's state
# 2. Passed into capa_problem via constructor
# 3. Assign from the OS's random number generator
self.seed = state.get('seed', seed)
if self.seed is None:
self.seed = struct.unpack('i', os.urandom(4))[0]
assert self.seed is not None, "Seed must be provided for LoncapaProblem."
self.student_answers = state.get('student_answers', {})
if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map'])
self.done = state.get('done', False)
self.input_state = state.get('input_state', {})
# Convert startouttext and endouttext to proper <text></text>
problem_text = re.sub("startouttext\s*/", "text", problem_text)
problem_text = re.sub("endouttext\s*/", "/text", problem_text)
......@@ -144,7 +117,7 @@ class LoncapaProblem(object):
self._process_includes()
# construct script processor context (eg for customresponse problems)
self.context = self._extract_context(self.tree, seed=self.seed)
self.context = self._extract_context(self.tree)
# Pre-parse the XML tree: modifies it to add ID's and perform some in-place
# transformations. This also creates the dict (self.responders) of Response
......@@ -440,18 +413,23 @@ class LoncapaProblem(object):
path = []
for dir in raw_path:
if not dir:
continue
# path is an absolute path or a path relative to the data dir
dir = os.path.join(self.system.filestore.root_path, dir)
# Check that we are within the filestore tree.
reldir = os.path.relpath(dir, self.system.filestore.root_path)
if ".." in reldir:
log.warning("Ignoring Python directory outside of course: %r" % dir)
continue
abs_dir = os.path.normpath(dir)
path.append(abs_dir)
return path
def _extract_context(self, tree, seed=struct.unpack('i', os.urandom(4))[0]): # private
def _extract_context(self, tree):
'''
Extract content of <script>...</script> from the problem.xml file, and exec it in the
context of this problem. Provides ability to randomize problems, and also set
......@@ -459,55 +437,47 @@ class LoncapaProblem(object):
Problem XML goes to Python execution context. Runs everything in script tags.
'''
random.seed(self.seed)
# save global context in here also
context = {'global_context': global_context}
context = {}
context['seed'] = self.seed
all_code = ''
# initialize context to have stuff in global_context
context.update(global_context)
python_path = []
# put globals there also
context['__builtins__'] = globals()['__builtins__']
# pass instance of LoncapaProblem in
context['the_lcp'] = self
context['script_code'] = ''
self._execute_scripts(tree.findall('.//script'), context)
return context
def _execute_scripts(self, scripts, context):
'''
Executes scripts in the given context.
'''
original_path = sys.path
for script in scripts:
sys.path = original_path + self._extract_system_path(script)
for script in tree.findall('.//script'):
stype = script.get('type')
if stype:
if 'javascript' in stype:
continue # skip javascript
if 'perl' in stype:
continue # skip perl
# TODO: evaluate only python
code = script.text
for d in self._extract_system_path(script):
if d not in python_path and os.path.exists(d):
python_path.append(d)
XMLESC = {"&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
......
......@@ -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
......
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.
"""Capa's specialized use of codejail.safe_exec."""
from .safe_exec import safe_exec, update_hash
"""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]
"""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
"""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\"')
"""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)
import fs
import fs.osfs
import os
import os, os.path
from capa.capa_problem import LoncapaProblem
from mock import Mock, MagicMock
import xml.sax.saxutils as saxutils
......@@ -22,16 +22,28 @@ def calledback_url(dispatch = 'score_update'):
xqueue_interface = MagicMock()
xqueue_interface.send_to_queue.return_value = (0, 'Success!')
test_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student'
)
def test_system():
"""
Construct a mock ModuleSystem instance.
"""
the_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student',
cache=None,
can_execute_unsafe_code=lambda: False,
)
return the_system
def new_loncapa_problem(xml, system=None):
"""Construct a `LoncapaProblem` suitable for unit tests."""
return LoncapaProblem(xml, id='1', seed=723, system=system or test_system())
......@@ -221,6 +221,8 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
cfn = kwargs.get('cfn', None)
expect = kwargs.get('expect', None)
answer = kwargs.get('answer', None)
options = kwargs.get('options', None)
cfn_extra_args = kwargs.get('cfn_extra_args', None)
# Create the response element
response_element = etree.Element("customresponse")
......@@ -235,6 +237,33 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer_element = etree.SubElement(response_element, "answer")
answer_element.text = str(answer)
if options:
response_element.set('options', str(options))
if cfn_extra_args:
response_element.set('cfn_extra_args', str(cfn_extra_args))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class SymbolicResponseXMLFactory(ResponseXMLFactory):
""" Factory for creating <symbolicresponse> XML trees """
def create_response_element(self, **kwargs):
cfn = kwargs.get('cfn', None)
answer = kwargs.get('answer', None)
options = kwargs.get('options', None)
response_element = etree.Element("symbolicresponse")
if cfn:
response_element.set('cfn', str(cfn))
if answer:
response_element.set('answer', str(answer))
if options:
response_element.set('options', str(options))
return response_element
def create_input_element(self, **kwargs):
......@@ -638,12 +667,16 @@ class StringResponseXMLFactory(ResponseXMLFactory):
Where *hint_prompt* is the string for which we show the hint,
*hint_name* is an internal identifier for the hint,
and *hint_text* is the text we show for the hint.
*hintfn*: The name of a function in the script to use for hints.
"""
# Retrieve the **kwargs
answer = kwargs.get("answer", None)
case_sensitive = kwargs.get("case_sensitive", True)
hint_list = kwargs.get('hints', None)
assert(answer)
hint_fn = kwargs.get('hintfn', None)
assert answer
# Create the <stringresponse> element
response_element = etree.Element("stringresponse")
......@@ -655,18 +688,24 @@ class StringResponseXMLFactory(ResponseXMLFactory):
response_element.set("type", "cs" if case_sensitive else "ci")
# Add the hints if specified
if hint_list:
if hint_list or hint_fn:
hintgroup_element = etree.SubElement(response_element, "hintgroup")
for (hint_prompt, hint_name, hint_text) in hint_list:
stringhint_element = etree.SubElement(hintgroup_element, "stringhint")
stringhint_element.set("answer", str(hint_prompt))
stringhint_element.set("name", str(hint_name))
if hint_list:
assert not hint_fn
for (hint_prompt, hint_name, hint_text) in hint_list:
stringhint_element = etree.SubElement(hintgroup_element, "stringhint")
stringhint_element.set("answer", str(hint_prompt))
stringhint_element.set("name", str(hint_name))
hintpart_element = etree.SubElement(hintgroup_element, "hintpart")
hintpart_element.set("on", str(hint_name))
hintpart_element = etree.SubElement(hintgroup_element, "hintpart")
hintpart_element.set("on", str(hint_name))
hint_text_element = etree.SubElement(hintpart_element, "text")
hint_text_element.text = str(hint_text)
hint_text_element = etree.SubElement(hintpart_element, "text")
hint_text_element.text = str(hint_text)
if hint_fn:
assert not hint_list
hintgroup_element.set("hintfn", hint_fn)
return response_element
......@@ -705,3 +744,38 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory):
option_element.text = description
return input_element
class SymbolicResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <symbolicresponse> xml """
def create_response_element(self, **kwargs):
""" Build the <symbolicresponse> XML element.
Uses **kwargs:
*expect*: The correct answer (a sympy string)
*options*: list of option strings to pass to symmath_check
(e.g. 'matrix', 'qbit', 'imaginary', 'numerical')"""
# Retrieve **kwargs
expect = kwargs.get('expect', '')
options = kwargs.get('options', [])
# Symmath check expects a string of options
options_str = ",".join(options)
# Construct the <symbolicresponse> element
response_element = etree.Element('symbolicresponse')
if expect:
response_element.set('expect', str(expect))
if options_str:
response_element.set('options', str(options_str))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
......@@ -26,7 +26,7 @@ class HelperTest(unittest.TestCase):
Make sure that our helper function works!
'''
def check(self, d):
xml = etree.XML(test_system.render_template('blah', d))
xml = etree.XML(test_system().render_template('blah', d))
self.assertEqual(d, extract_context(xml))
def test_extract_context(self):
......@@ -46,11 +46,11 @@ class SolutionRenderTest(unittest.TestCase):
xml_str = """<solution id="solution_12">{s}</solution>""".format(s=solution)
element = etree.fromstring(xml_str)
renderer = lookup_tag('solution')(test_system, element)
renderer = lookup_tag('solution')(test_system(), element)
self.assertEqual(renderer.id, 'solution_12')
# our test_system "renders" templates to a div with the repr of the context
# Our test_system "renders" templates to a div with the repr of the context.
xml = renderer.get_html()
context = extract_context(xml)
self.assertEqual(context, {'id': 'solution_12'})
......@@ -65,7 +65,7 @@ class MathRenderTest(unittest.TestCase):
xml_str = """<math>{tex}</math>""".format(tex=latex_in)
element = etree.fromstring(xml_str)
renderer = lookup_tag('math')(test_system, element)
renderer = lookup_tag('math')(test_system(), element)
self.assertEqual(renderer.mathstr, mathjax_out)
......
......@@ -6,12 +6,15 @@ import json
import mock
from capa.capa_problem import LoncapaProblem
from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from . import test_system
from . import test_system, new_loncapa_problem
class CapaHtmlRenderTest(unittest.TestCase):
def setUp(self):
super(CapaHtmlRenderTest, self).setUp()
self.system = test_system()
def test_blank_problem(self):
"""
It's important that blank problems don't break, since that's
......@@ -20,7 +23,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
xml_str = "<problem> </problem>"
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
......@@ -39,7 +42,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str, system=self.system)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
......@@ -49,9 +52,6 @@ class CapaHtmlRenderTest(unittest.TestCase):
self.assertEqual(test_element.tag, "test")
self.assertEqual(test_element.text, "Test include")
def test_process_outtext(self):
# Generate some XML with <startouttext /> and <endouttext />
xml_str = textwrap.dedent("""
......@@ -61,7 +61,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
......@@ -80,7 +80,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
......@@ -98,7 +98,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Render the HTML
rendered_html = etree.XML(problem.get_html())
......@@ -117,11 +117,12 @@ class CapaHtmlRenderTest(unittest.TestCase):
xml_str = StringResponseXMLFactory().build_xml(**kwargs)
# Mock out the template renderer
test_system.render_template = mock.Mock()
test_system.render_template.return_value = "<div>Input Template Render</div>"
the_system = test_system()
the_system.render_template = mock.Mock()
the_system.render_template.return_value = "<div>Input Template Render</div>"
# Create the problem and render the HTML
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str, system=the_system)
rendered_html = etree.XML(problem.get_html())
# Expect problem has been turned into a <div>
......@@ -166,7 +167,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
mock.call('textline.html', expected_textline_context),
mock.call('solutionspan.html', expected_solution_context)]
self.assertEqual(test_system.render_template.call_args_list,
self.assertEqual(the_system.render_template.call_args_list,
expected_calls)
......@@ -184,7 +185,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
xml_str = CustomResponseXMLFactory().build_xml(**kwargs)
# Create the problem and render the html
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
# Grade the problem
correctmap = problem.grade_answers({'1_2_1': 'test'})
......@@ -219,7 +220,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
""")
# Create the problem and render the HTML
problem = LoncapaProblem(xml_str, '1', system=test_system)
problem = new_loncapa_problem(xml_str)
rendered_html = etree.XML(problem.get_html())
# Expect that the variable $test has been replaced with its value
......@@ -227,7 +228,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
self.assertEqual(span_element.get('attr'), "TEST")
def _create_test_file(self, path, content_str):
test_fp = test_system.filestore.open(path, "w")
test_fp = self.system.filestore.open(path, "w")
test_fp.write(content_str)
test_fp.close()
......
......@@ -45,7 +45,7 @@ class OptionInputTest(unittest.TestCase):
state = {'value': 'Down',
'id': 'sky_input',
'status': 'answered'}
option_input = lookup_tag('optioninput')(test_system, element, state)
option_input = lookup_tag('optioninput')(test_system(), element, state)
context = option_input._get_render_context()
......@@ -92,7 +92,7 @@ class ChoiceGroupTest(unittest.TestCase):
'id': 'sky_input',
'status': 'answered'}
the_input = lookup_tag(tag)(test_system, element, state)
the_input = lookup_tag(tag)(test_system(), element, state)
context = the_input._get_render_context()
......@@ -142,7 +142,7 @@ class JavascriptInputTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': '3', }
the_input = lookup_tag('javascriptinput')(test_system, element, state)
the_input = lookup_tag('javascriptinput')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -170,7 +170,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state)
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -198,7 +198,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state)
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -236,7 +236,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system, element, state)
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -274,7 +274,7 @@ class FileSubmissionTest(unittest.TestCase):
'status': 'incomplete',
'feedback': {'message': '3'}, }
input_class = lookup_tag('filesubmission')
the_input = input_class(test_system, element, state)
the_input = input_class(test_system(), element, state)
context = the_input._get_render_context()
......@@ -319,7 +319,7 @@ class CodeInputTest(unittest.TestCase):
'feedback': {'message': '3'}, }
input_class = lookup_tag('codeinput')
the_input = input_class(test_system, element, state)
the_input = input_class(test_system(), element, state)
context = the_input._get_render_context()
......@@ -368,7 +368,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
self.input_class = lookup_tag('matlabinput')
self.the_input = self.input_class(test_system, elt, state)
self.the_input = self.input_class(test_system(), elt, state)
def test_rendering(self):
context = self.the_input._get_render_context()
......@@ -396,7 +396,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
......@@ -423,7 +423,7 @@ class MatlabTest(unittest.TestCase):
}
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
......@@ -448,7 +448,7 @@ class MatlabTest(unittest.TestCase):
}
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
......@@ -470,7 +470,7 @@ class MatlabTest(unittest.TestCase):
get = {'submission': 'x = 1234;'}
response = self.the_input.handle_ajax("plot", get)
test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
test_system().xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
self.assertTrue(response['success'])
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
......@@ -479,13 +479,12 @@ class MatlabTest(unittest.TestCase):
def test_plot_data_failure(self):
get = {'submission': 'x = 1234;'}
error_message = 'Error message!'
test_system.xqueue['interface'].send_to_queue.return_value = (1, error_message)
test_system().xqueue['interface'].send_to_queue.return_value = (1, error_message)
response = self.the_input.handle_ajax("plot", get)
self.assertFalse(response['success'])
self.assertEqual(response['message'], error_message)
self.assertTrue('queuekey' not in self.the_input.input_state)
self.assertTrue('queuestate' not in self.the_input.input_state)
test_system.xqueue['interface'].send_to_queue.return_value = (0, 'Success!')
def test_ungraded_response_success(self):
queuekey = 'abcd'
......@@ -496,7 +495,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
......@@ -514,7 +513,7 @@ class MatlabTest(unittest.TestCase):
'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system, elt, state)
the_input = self.input_class(test_system(), elt, state)
inner_msg = 'hello!'
queue_msg = json.dumps({'msg': inner_msg})
......@@ -553,7 +552,7 @@ class SchematicTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('schematic')(test_system, element, state)
the_input = lookup_tag('schematic')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -592,7 +591,7 @@ class ImageInputTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('imageinput')(test_system, element, state)
the_input = lookup_tag('imageinput')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -643,7 +642,7 @@ class CrystallographyTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('crystallography')(test_system, element, state)
the_input = lookup_tag('crystallography')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -681,7 +680,7 @@ class VseprTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = lookup_tag('vsepr_input')(test_system, element, state)
the_input = lookup_tag('vsepr_input')(test_system(), element, state)
context = the_input._get_render_context()
......@@ -708,7 +707,7 @@ class ChemicalEquationTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'H2OYeah', }
self.the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
self.the_input = lookup_tag('chemicalequationinput')(test_system(), element, state)
def test_rendering(self):
''' Verify that the render context matches the expected render context'''
......@@ -783,7 +782,7 @@ class DragAndDropTest(unittest.TestCase):
]
}
the_input = lookup_tag('drag_and_drop_input')(test_system, element, state)
the_input = lookup_tag('drag_and_drop_input')(test_system(), element, state)
context = the_input._get_render_context()
expected = {'id': 'prob_1_2',
......@@ -832,7 +831,7 @@ class AnnotationInputTest(unittest.TestCase):
tag = 'annotationinput'
the_input = lookup_tag(tag)(test_system, element, state)
the_input = lookup_tag(tag)(test_system(), element, state)
context = the_input._get_render_context()
......
from .calc import evaluator, UndefinedVariable
from calc import evaluator, UndefinedVariable
from cmath import isinf
#-----------------------------------------------------------------------------
......
......@@ -4,5 +4,5 @@ setup(
name="capa",
version="0.1",
packages=find_packages(exclude=["tests"]),
install_requires=['distribute==0.6.30', 'pyparsing==1.5.6'],
install_requires=["distribute==0.6.28"],
)
......@@ -736,4 +736,4 @@ def test6(): # imaginary numbers
</mstyle>
</math>
'''
return formula(xmlstr, options='imaginaryi')
return formula(xmlstr, options='imaginary')
......@@ -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}
from setuptools import setup
setup(
name="chem",
version="0.1",
packages=["chem"],
install_requires=[
"pyparsing==1.5.6",
"numpy",
"scipy",
"nltk==2.0.4",
],
)
This directory is in the Python path for sandboxed Python execution.
from setuptools import setup
setup(
name="sandbox-packages",
version="0.1",
packages=[
"verifiers",
],
py_modules=[
"eia",
],
install_requires=[
],
)
......@@ -13,13 +13,10 @@ real time, next to the input box.
<p>This is a correct answer which may be entered below: </p>
<p><tt>cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]</tt></p>
<script>
from symmath import *
</script>
<text>Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 &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/>
......
......@@ -3,7 +3,9 @@ import datetime
import hashlib
import json
import logging
import os
import traceback
import struct
import sys
from pkg_resources import resource_string
......@@ -23,8 +25,10 @@ from xmodule.util.date_utils import time_to_datetime
log = logging.getLogger("mitx.courseware")
# Generated this many different variants of problems with rerandomize=per_student
# Generate this many different variants of problems with rerandomize=per_student
NUM_RANDOMIZATION_BINS = 20
# Never produce more than this many different seeds, no matter what.
MAX_RANDOMIZATION_BINS = 1000
def randomization_bin(seed, problem_id):
......@@ -128,11 +132,7 @@ class CapaModule(CapaFields, XModule):
self.close_date = due_date
if self.seed is None:
if self.rerandomize == 'never':
self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
# see comment on randomization_bin
self.seed = randomization_bin(system.seed, self.location.url)
self.choose_new_seed()
# Need the problem location in openendedresponse to send out. Adding
# it to the system here seems like the least clunky way to get it
......@@ -176,6 +176,22 @@ class CapaModule(CapaFields, XModule):
self.set_state_from_lcp()
assert self.seed is not None
def choose_new_seed(self):
"""Choose a new seed."""
if self.rerandomize == 'never':
self.seed = 1
elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
# see comment on randomization_bin
self.seed = randomization_bin(self.system.seed, self.location.url)
else:
self.seed = struct.unpack('i', os.urandom(4))[0]
# So that sandboxed code execution can be cached, but still have an interesting
# number of possibilities, cap the number of different random seeds.
self.seed %= MAX_RANDOMIZATION_BINS
def new_lcp(self, state, text=None):
if text is None:
text = self.data
......@@ -184,6 +200,7 @@ class CapaModule(CapaFields, XModule):
problem_text=text,
id=self.location.html_id(),
state=state,
seed=self.seed,
system=self.system,
)
......@@ -851,14 +868,11 @@ class CapaModule(CapaFields, XModule):
'error': "Refresh the page and make an attempt before resetting."}
if self.rerandomize in ["always", "onreset"]:
# reset random number generator seed (note the self.lcp.get_state()
# in next line)
seed = None
else:
seed = self.lcp.seed
# Reset random number generator seed.
self.choose_new_seed()
# Generate a new problem with either the previous seed or a new seed
self.lcp = self.new_lcp({'seed': seed})
self.lcp = self.new_lcp(None)
# Pull in the new problem seed
self.set_state_from_lcp()
......
......@@ -31,11 +31,11 @@ class ModuleStoreTestCase(TestCase):
@staticmethod
def load_templates_if_necessary():
'''
Load templates into the modulestore only if they do not already exist.
Load templates into the direct modulestore only if they do not already exist.
We need the templates, because they are copied to create
XModules such as sections and problems
'''
modulestore = xmodule.modulestore.django.modulestore()
modulestore = xmodule.modulestore.django.modulestore('direct')
# Count the number of templates
query = {"_id.course": "templates"}
......
......@@ -14,7 +14,7 @@ import fs.osfs
import numpy
import capa.calc as calc
import calc
import xmodule
from xmodule.x_module import ModuleSystem
from mock import Mock
......@@ -33,15 +33,14 @@ def test_system():
"""
Construct a test ModuleSystem instance.
By default, the render_template() method simply returns
the context it is passed as a string.
You can override this behavior by monkey patching:
By default, the render_template() method simply returns the context it is
passed as a string. You can override this behavior by monkey patching::
system = test_system()
system.render_template = my_render_func
system = test_system()
system.render_template = my_render_func
where `my_render_func` is a function of the form my_render_func(template, context).
where my_render_func is a function of the form
my_render_func(template, context)
"""
return ModuleSystem(
ajax_url='courses/course_id/modx/a_location',
......@@ -86,10 +85,12 @@ class ModelsTest(unittest.TestCase):
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)") + 1) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1") - 0.5 - 0.5j) < 0.00001)
variables['t'] = 1.0
# Use self.assertAlmostEqual here...
self.assertTrue(abs(calc.evaluator(variables, functions, "t") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True) - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True) - 298) < 0.2)
# Use self.assertRaises here...
exception_happened = False
try:
calc.evaluator({}, {}, "5+7 QWSEKO")
......
......@@ -550,6 +550,7 @@ class CapaModuleTest(unittest.TestCase):
def test_reset_problem(self):
module = CapaFactory.create(done=True)
module.new_lcp = Mock(wraps=module.new_lcp)
module.choose_new_seed = Mock(wraps=module.choose_new_seed)
# Stub out HTML rendering
with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
......@@ -567,7 +568,8 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(result['html'], "<div>Test HTML</div>")
# Expect that the problem was reset
module.new_lcp.assert_called_once_with({'seed': None})
module.new_lcp.assert_called_once_with(None)
module.choose_new_seed.assert_called_once_with()
def test_reset_problem_closed(self):
module = CapaFactory.create()
......@@ -1033,3 +1035,13 @@ class CapaModuleTest(unittest.TestCase):
self.assertTrue(module.seed is not None)
msg = 'Could not get a new seed from reset after 5 tries'
self.assertTrue(success, msg)
def test_random_seed_bins(self):
# Assert that we are limiting the number of possible seeds.
# Check the conditions that generate random seeds
for rerandomize in ['always', 'per_student', 'true', 'onreset']:
# Get a bunch of seeds, they should all be in 0-999.
for i in range(200):
module = CapaFactory.create(rerandomize=rerandomize)
assert 0 <= module.seed < 1000
......@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase):
'''
def test_xmodule_default(self):
'''Make sure default get_progress exists, returns None'''
xm = x_module.XModule(test_system, 'a://b/c/d/e', None, {})
xm = x_module.XModule(test_system(), 'a://b/c/d/e', None, {})
p = xm.get_progress()
self.assertEqual(p, None)
......@@ -14,7 +14,6 @@ START = '2013-01-01T01:00:00'
from .test_course_module import DummySystem as DummyImportSystem
from . import test_system
class RandomizeModuleTestCase(unittest.TestCase):
......
......@@ -763,7 +763,10 @@ class ModuleSystem(object):
anonymous_student_id='',
course_id=None,
open_ended_grading_interface=None,
s3_interface=None):
s3_interface=None,
cache=None,
can_execute_unsafe_code=None,
):
'''
Create a closure around the system environment.
......@@ -805,6 +808,14 @@ class ModuleSystem(object):
xblock_model_data - A dict-like object containing the all data available to this
xblock
cache - A cache object with two methods:
.get(key) returns an object from the cache or None.
.set(key, value, timeout_secs=None) stores a value in the cache with a timeout.
can_execute_unsafe_code - A function returning a boolean, whether or
not to allow the execution of unsafe, unsandboxed code.
'''
self.ajax_url = ajax_url
self.xqueue = xqueue
......@@ -829,6 +840,9 @@ class ModuleSystem(object):
self.open_ended_grading_interface = open_ended_grading_interface
self.s3_interface = s3_interface
self.cache = cache or DoNothingCache()
self.can_execute_unsafe_code = can_execute_unsafe_code or (lambda: False)
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
return self.__dict__.get(attr)
......@@ -842,3 +856,12 @@ class ModuleSystem(object):
def __str__(self):
return str(self.__dict__)
class DoNothingCache(object):
"""A duck-compatible object to use in ModuleSystem when there's no cache."""
def get(self, key):
return None
def set(self, key, value, timeout=None):
pass
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
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 []
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 []
describe 'Logger', ->
it 'expose window.log_event', ->
jasmine.stubRequests()
expect(window.log_event).toBe Logger.log
describe 'log', ->
......@@ -12,7 +11,8 @@ describe 'Logger', ->
event: '"data"'
page: window.location.href
describe 'bind', ->
# Broken with commit 9f75e64? Skipping for now.
xdescribe 'bind', ->
beforeEach ->
Logger.bind()
Courseware.prefix = '/6002x'
......
......@@ -88,20 +88,32 @@ if Backbone?
pinned = @get("pinned")
@set("pinned",pinned)
@trigger "change", @
flagAbuse: ->
temp_array = @get("abuse_flaggers")
temp_array.push(window.user.get('id'))
@set("abuse_flaggers",temp_array)
@trigger "change", @
unflagAbuse: ->
@get("abuse_flaggers").pop(window.user.get('id'))
@trigger "change", @
class @Thread extends @Content
urlMappers:
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @discussion.id, @id)
'reply' : -> DiscussionUtil.urlFor('create_comment', @id)
'unvote' : -> DiscussionUtil.urlFor("undo_vote_for_#{@get('type')}", @id)
'upvote' : -> DiscussionUtil.urlFor("upvote_#{@get('type')}", @id)
'downvote' : -> DiscussionUtil.urlFor("downvote_#{@get('type')}", @id)
'close' : -> DiscussionUtil.urlFor('openclose_thread', @id)
'update' : -> DiscussionUtil.urlFor('update_thread', @id)
'delete' : -> DiscussionUtil.urlFor('delete_thread', @id)
'follow' : -> DiscussionUtil.urlFor('follow_thread', @id)
'unfollow' : -> DiscussionUtil.urlFor('unfollow_thread', @id)
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
'pinThread' : -> DiscussionUtil.urlFor("pin_thread", @id)
'unPinThread' : -> DiscussionUtil.urlFor("un_pin_thread", @id)
......@@ -157,6 +169,8 @@ if Backbone?
'endorse': -> DiscussionUtil.urlFor('endorse_comment', @id)
'update': -> DiscussionUtil.urlFor('update_comment', @id)
'delete': -> DiscussionUtil.urlFor('delete_comment', @id)
'flagAbuse' : -> DiscussionUtil.urlFor("flagAbuse_#{@get('type')}", @id)
'unFlagAbuse' : -> DiscussionUtil.urlFor("unFlagAbuse_#{@get('type')}", @id)
getCommentsCount: ->
count = 0
......
......@@ -37,6 +37,9 @@ if Backbone?
data['commentable_ids'] = options.commentable_ids
when 'all'
url = DiscussionUtil.urlFor 'threads'
when 'flagged'
data['flagged'] = true
url = DiscussionUtil.urlFor 'search'
when 'followed'
url = DiscussionUtil.urlFor 'followed_threads', options.user_id
if options['group_id']
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment