Commit 78acd083 by David Ormsbee

Merge branch 'master' into ormsbee/verifyuser3

Conflicts:
	common/djangoapps/course_modes/models.py
	lms/djangoapps/shoppingcart/models.py
	lms/djangoapps/shoppingcart/processors/CyberSource.py
	lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py
	lms/djangoapps/shoppingcart/tests/test_models.py
	lms/djangoapps/shoppingcart/tests/test_views.py
	lms/djangoapps/shoppingcart/urls.py
	lms/djangoapps/shoppingcart/views.py
	lms/envs/common.py
	lms/envs/dev.py
	lms/static/sass/base/_variables.scss
parents 35a7b75e 6defd7ba
......@@ -46,3 +46,4 @@ autodeploy.properties
.ws_migrations_complete
.vagrant/
logs
.testids/
[pep8]
ignore=E501
\ No newline at end of file
ignore=E501
exclude=migrations
\ No newline at end of file
......@@ -84,3 +84,5 @@ Mukul Goyal <miki@edx.org>
Robert Marks <rmarks@edx.org>
Yarko Tymciurak <yarkot1@gmail.com>
Miles Steele <miles@milessteele.com>
Kevin Luo <kevluo@edx.org>
Akshay Jagadeesh <akjags@gmail.com>
......@@ -5,9 +5,15 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
LMS: Added alphabetical sorting of forum categories and subcategories.
It is hidden behind a false defaulted course level flag.
Studio: Allow course authors to set their course image on the schedule
and details page, with support for JPEG and PNG images.
LMS, Studio: Centralized startup code to manage.py and wsgi.py files.
Made studio runnable using wsgi.
Blades: Took videoalpha out of alpha, replacing the old video player
Common: Allow instructors to input complicated expressions as answers to
......@@ -27,6 +33,9 @@ logic has been consolidated into the model -- you should use new class methods
to `enroll()`, `unenroll()`, and to check `is_enrolled()`, instead of creating
CourseEnrollment objects or querying them directly.
LMS: Added bulk email for course feature, with option to optout of individual
course emails.
Studio: Email will be sent to admin address when a user requests course creator
privileges for Studio (edge only).
......
......@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches
from nose.tools import assert_false, assert_equal, assert_regexp_matches # pylint: disable=E0611
from common import type_in_codemirror, press_the_notification_button
KEY_CSS = '.key input.policy-key'
......
......@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true, assert_equal, assert_in
from nose.tools import assert_true, assert_equal, assert_in # pylint: disable=E0611
from terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException
......
......@@ -2,7 +2,7 @@
# pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true
from nose.tools import assert_true # pylint: disable=E0611
from auth.authz import get_user_by_email, get_course_groupname_for_role
from django.conf import settings
......@@ -265,9 +265,8 @@ def type_in_codemirror(index, text):
def upload_file(filename):
file_css = '.upload-dialog input[type=file]'
upload = world.css_find(file_css).first
path = os.path.join(TEST_ROOT, filename)
upload._element.send_keys(os.path.abspath(path))
world.browser.execute_script("$('input.file-input').css('display', 'block')")
world.browser.attach_file('file', os.path.abspath(path))
button_css = '.upload-dialog .action-upload'
world.css_click(button_css)
......@@ -2,7 +2,7 @@
#pylint: disable=W0621
from lettuce import world, step
from nose.tools import assert_true
from nose.tools import assert_true # pylint: disable=E0611
DATA_LOCATION = 'i4x://edx/templates'
......
......@@ -2,7 +2,7 @@
#pylint: disable=C0111
from lettuce import world
from nose.tools import assert_equal
from nose.tools import assert_equal # pylint: disable=E0611
from terrain.steps import reload_the_page
......
......@@ -3,7 +3,7 @@
from lettuce import world, step
from common import *
from nose.tools import assert_true, assert_false, assert_equal
from nose.tools import assert_true, assert_false, assert_equal # pylint: disable=E0611
from logging import getLogger
logger = getLogger(__name__)
......
......@@ -7,7 +7,7 @@ from selenium.webdriver.common.keys import Keys
from common import type_in_codemirror, upload_file
from django.conf import settings
from nose.tools import assert_true, assert_false, assert_equal
from nose.tools import assert_true, assert_false, assert_equal # pylint: disable=E0611
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
......@@ -168,15 +168,18 @@ def i_see_new_course_image(_step):
img = images[0]
expected_src = '/c4x/MITx/999/asset/image.jpg'
# Don't worry about the domain in the URL
assert img['src'].endswith(expected_src)
try:
assert img['src'].endswith(expected_src)
except AssertionError as e:
e.args += ('Was looking for {}'.format(expected_src), 'Found {}'.format(img['src']))
raise
@step('the image URL should be present in the field')
def image_url_present(_step):
field_css = '#course-image-url'
field = world.css_find(field_css).first
expected_value = '/c4x/MITx/999/asset/image.jpg'
assert field.value == expected_value
assert world.css_value(field_css) == expected_value
############### HELPER METHODS ####################
......
......@@ -5,7 +5,7 @@ from lettuce import world, step
from common import create_studio_user
from django.contrib.auth.models import Group
from auth.authz import get_course_groupname_for_role, get_user_by_email
from nose.tools import assert_true
from nose.tools import assert_true # pylint: disable=E0611
PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org'
......
......@@ -45,3 +45,25 @@ Feature: Course updates
When I modify the handout to "<ol>Test</ol>"
Then I see the handout "Test"
And I see a "saving" notification
Scenario: Static links are rewritten when previewing a course update
Given I have opened a new course in Studio
And I go to the course updates page
When I add a new update with the text "<img src='/static/my_img.jpg'/>"
# Can only do partial text matches because of the quotes with in quotes (and regexp step matching).
Then I should see the update "/c4x/MITx/999/asset/my_img.jpg"
And I change the update from "/static/my_img.jpg" to "<img src='/static/modified.jpg'/>"
Then I should see the update "/c4x/MITx/999/asset/modified.jpg"
And when I reload the page
Then I should see the update "/c4x/MITx/999/asset/modified.jpg"
Scenario: Static links are rewritten when previewing handouts
Given I have opened a new course in Studio
And I go to the course updates page
When I modify the handout to "<ol><img src='/static/my_img.jpg'/></ol>"
# Can only do partial text matches because of the quotes with in quotes (and regexp step matching).
Then I see the handout "/c4x/MITx/999/asset/my_img.jpg"
And I change the handout from "/static/my_img.jpg" to "<img src='/static/modified.jpg'/>"
Then I see the handout "/c4x/MITx/999/asset/modified.jpg"
And when I reload the page
Then I see the handout "/c4x/MITx/999/asset/modified.jpg"
......@@ -38,6 +38,16 @@ def modify_update(_step, text):
change_text(text)
@step(u'I change the update from "([^"]*)" to "([^"]*)"$')
def change_existing_update(_step, before, after):
verify_text_in_editor_and_update('div.post-preview a.edit-button', before, after)
@step(u'I change the handout from "([^"]*)" to "([^"]*)"$')
def change_existing_handout(_step, before, after):
verify_text_in_editor_and_update('div.course-handouts a.edit-button', before, after)
@step(u'I delete the update$')
def click_button(_step):
button_css = 'div.post-preview a.delete-button'
......@@ -80,3 +90,10 @@ def change_text(text):
type_in_codemirror(0, text)
save_css = 'a.save-button'
world.css_click(save_css)
def verify_text_in_editor_and_update(button_css, before, after):
world.css_click(button_css)
text = world.css_find(".cm-string").html
assert before in text
change_text(after)
......@@ -2,7 +2,7 @@
#pylint: disable=C0111
from lettuce import world, step
from nose.tools import assert_equal
from nose.tools import assert_equal # pylint: disable=E0611
from common import type_in_codemirror
DISPLAY_NAME = "Display Name"
......
......@@ -3,7 +3,7 @@
from lettuce import world, step
from common import *
from nose.tools import assert_equal
from nose.tools import assert_equal # pylint: disable=E0611
############### ACTIONS ####################
......
......@@ -11,8 +11,9 @@ Feature: Static Pages
Given I have opened a new course in Studio
And I go to the static pages page
And I add a new page
When I will confirm all alerts
And I "delete" the "Empty" page
Then I am shown a prompt
When I confirm the prompt
Then I should not see a "Empty" static page
# Safari won't update the name properly
......
......@@ -3,7 +3,7 @@
from lettuce import world, step
from common import *
from nose.tools import assert_equal
from nose.tools import assert_equal # pylint: disable=E0611
############### ACTIONS ####################
......
......@@ -29,7 +29,7 @@ def correct_video_settings(_step):
['Download Track', '', False],
['Download Video', '', False],
['End Time', '0', False],
['HTML5 Subtitles', '', False],
['HTML5 Timed Transcript', '', False],
['Show Captions', 'True', False],
['Start Time', '0', False],
['Video Sources', '', False],
......
......@@ -3,11 +3,6 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import check_module_metadata_editability
from xmodule.course_module import CourseDescriptor
from request_cache.middleware import RequestCache
from django.core.cache import get_cache
CACHE = get_cache('mongo_metadata_inheritance')
class Command(BaseCommand):
help = '''Enumerates through the course and find common errors'''
......@@ -21,12 +16,6 @@ class Command(BaseCommand):
loc = CourseDescriptor.id_to_location(loc_str)
store = modulestore()
# setup a request cache so we don't throttle the DB with all the metadata inheritance requests
store.set_modulestore_configuration({
'metadata_inheritance_cache_subsystem': CACHE,
'request_cache': RequestCache.get_request_cache()
})
course = store.get_item(loc, depth=3)
err_cnt = 0
......
......@@ -9,14 +9,10 @@ from xmodule.course_module import CourseDescriptor
from auth.authz import _copy_course_group
#
# To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3
#
from request_cache.middleware import RequestCache
from django.core.cache import get_cache
CACHE = get_cache('mongo_metadata_inheritance')
class Command(BaseCommand):
"""Clone a MongoDB-backed course to another location"""
help = 'Clone a MongoDB backed course to another location'
......@@ -32,11 +28,6 @@ class Command(BaseCommand):
mstore = modulestore('direct')
cstore = contentstore()
mstore.set_modulestore_configuration({
'metadata_inheritance_cache_subsystem': CACHE,
'request_cache': RequestCache.get_request_cache()
})
org, course_num, run = dest_course_id.split("/")
mstore.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num))
......
......@@ -9,14 +9,11 @@ from xmodule.course_module import CourseDescriptor
from .prompt import query_yes_no
from auth.authz import _delete_course_group
from request_cache.middleware import RequestCache
from django.core.cache import get_cache
#
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
#
CACHE = get_cache('mongo_metadata_inheritance')
class Command(BaseCommand):
help = '''Delete a MongoDB backed course'''
......@@ -36,11 +33,6 @@ class Command(BaseCommand):
ms = modulestore('direct')
cs = contentstore()
ms.set_modulestore_configuration({
'metadata_inheritance_cache_subsystem': CACHE,
'request_cache': RequestCache.get_request_cache()
})
org, course_num, run = course_id.split("/")
ms.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num))
......
"""
Define test configuration for modulestores.
"""
from xmodule.modulestore.tests.django_utils import studio_store_config
from django.conf import settings
TEST_MODULESTORE = studio_store_config(settings.TEST_ROOT / "data")
......@@ -60,11 +60,11 @@ class UploadTestCase(CourseTestCase):
f = BytesIO("sample content")
f.name = "sample.txt"
resp = self.client.post(self.url, {"name": "my-name", "file": f})
self.assert2XX(resp.status_code)
self.assertEquals(resp.status_code, 200)
def test_no_file(self):
resp = self.client.post(self.url, {"name": "file.txt"})
self.assert4XX(resp.status_code)
self.assertEquals(resp.status_code, 400)
def test_get(self):
resp = self.client.get(self.url)
......
......@@ -3,6 +3,9 @@
import json
import shutil
import mock
from textwrap import dedent
from django.test.client import Client
from django.test.utils import override_settings
from django.conf import settings
......@@ -22,6 +25,7 @@ from contentstore.tests.utils import parse_json
from auth.authz import add_user_to_creator_group
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location, mongo
......@@ -65,7 +69,7 @@ class MongoCollectionFindWrapper(object):
return self.original(query, *args, **kwargs)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase):
"""
Tests that rely on the toy courses.
......@@ -312,7 +316,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
self.assertIn('/static/', handouts.data)
def test_import_textbook_as_content_element(self):
@mock.patch('xmodule.course_module.requests.get')
def test_import_textbook_as_content_element(self, mock_get):
mock_get.return_value.text = dedent("""
<?xml version="1.0"?><table_of_contents>
<entry page="5" page_label="ii" name="Table of Contents"/>
</table_of_contents>
""").strip()
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'])
......@@ -845,7 +856,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
filesystem = OSFS(root_dir / ('test_export/' + dirname))
self.assertTrue(filesystem.exists(item.location.name + filename_suffix))
def test_export_course(self):
@mock.patch('xmodule.course_module.requests.get')
def test_export_course(self, mock_get):
mock_get.return_value.text = dedent("""
<?xml version="1.0"?><table_of_contents>
<entry page="5" page_label="ii" name="Table of Contents"/>
</table_of_contents>
""").strip()
module_store = modulestore('direct')
draft_store = modulestore('draft')
content_store = contentstore()
......@@ -1122,12 +1140,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
module_store.collection.find = wrapper.find
print module_store.metadata_inheritance_cache_subsystem
print module_store.request_cache
course = module_store.get_item(location, depth=2)
# make sure we haven't done too many round trips to DB
# note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and
# 4) because of the RT due to calculating the inherited metadata
self.assertEqual(wrapper.counter, 4)
# note we say 3 round trips here for 1) the course, and 2 & 3) for the chapters and sequentials
# Because we're querying from the top of the tree, we cache information needed for inheritance,
# so we don't need to make an extra query to compute it.
self.assertEqual(wrapper.counter, 3)
# make sure we pre-fetched a known sequential which should be at depth=2
self.assertTrue(Location(['i4x', 'edX', 'toy', 'sequential',
......@@ -1163,7 +1184,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreTest(ModuleStoreTestCase):
"""
Tests for the CMS ContentStore application.
......@@ -1408,7 +1429,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'Chapter 2')
# go to various pages
......@@ -1418,92 +1439,92 @@ class ContentStoreTest(ModuleStoreTestCase):
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# export page
resp = self.client.get(reverse('export_course',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# manage users
resp = self.client.get(reverse('manage_users',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# course info
resp = self.client.get(reverse('course_info',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# settings_details
resp = self.client.get(reverse('settings_details',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# settings_details
resp = self.client.get(reverse('settings_grading',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# static_pages
resp = self.client.get(reverse('static_pages',
kwargs={'org': loc.org,
'course': loc.course,
'coursename': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# static_pages
resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org,
'course': loc.course,
'name': loc.name}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence')
resp = self.client.get(reverse('edit_subsection',
kwargs={'location': subsection_location.url()}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# go look at the Edit page
unit_location = loc.replace(category='vertical', name='test_vertical')
resp = self.client.get(reverse('edit_unit',
kwargs={'location': unit_location.url()}))
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# delete a component
del_loc = loc.replace(category='html', name='test_html')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# delete a unit
del_loc = loc.replace(category='vertical', name='test_vertical')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# delete a unit
del_loc = loc.replace(category='sequential', name='test_sequence')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# delete a chapter
del_loc = loc.replace(category='chapter', name='chapter_2')
resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
def test_import_into_new_course_id(self):
module_store = modulestore('direct')
......@@ -1690,6 +1711,7 @@ class ContentStoreTest(ModuleStoreTestCase):
content_store.find(location)
@override_settings(MODULESTORE=TEST_MODULESTORE)
class MetadataSaveTestCase(ModuleStoreTestCase):
"""Test that metadata is correctly cached and decached."""
......
......@@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase):
def test_get(self):
resp = self.client.get(self.url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content)
def test_delete(self):
resp = self.client.delete(self.url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
def test_post(self):
grader = {
......@@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase):
"weight": 17.3,
}
resp = self.client.post(self.url, grader)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content)
......@@ -3,10 +3,13 @@ from unittest import skip
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.test.client import Client
from django.test.utils import override_settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE
@override_settings(MODULESTORE=TEST_MODULESTORE)
class InternationalizationTest(ModuleStoreTestCase):
"""
Tests to validate Internationalization.
......
"""
Unit tests for course import and export
"""
import os
import shutil
import tarfile
import tempfile
import copy
from uuid import uuid4
from pymongo import MongoClient
from .utils import CourseTestCase
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.conf import settings
from xmodule.contentstore.django import _CONTENTSTORE
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ImportTestCase(CourseTestCase):
"""
Unit tests for importing a course
"""
def setUp(self):
super(ImportTestCase, self).setUp()
self.url = reverse("import_course", kwargs={
'org': self.course.location.org,
'course': self.course.location.course,
'name': self.course.location.name,
})
self.content_dir = tempfile.mkdtemp()
def touch(name):
""" Equivalent to shell's 'touch'"""
with file(name, 'a'):
os.utime(name, None)
# Create tar test files -----------------------------------------------
# OK course:
good_dir = tempfile.mkdtemp(dir=self.content_dir)
os.makedirs(os.path.join(good_dir, "course"))
with open(os.path.join(good_dir, "course.xml"), "w+") as f:
f.write('<course url_name="2013_Spring" org="EDx" course="0.00x"/>')
with open(os.path.join(good_dir, "course", "2013_Spring.xml"), "w+") as f:
f.write('<course></course>')
self.good_tar = os.path.join(self.content_dir, "good.tar.gz")
with tarfile.open(self.good_tar, "w:gz") as gtar:
gtar.add(good_dir)
# Bad course (no 'course.xml' file):
bad_dir = tempfile.mkdtemp(dir=self.content_dir)
touch(os.path.join(bad_dir, "bad.xml"))
self.bad_tar = os.path.join(self.content_dir, "bad.tar.gz")
with tarfile.open(self.bad_tar, "w:gz") as btar:
btar.add(bad_dir)
def tearDown(self):
shutil.rmtree(self.content_dir)
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear()
def test_no_coursexml(self):
"""
Check that the response for a tar.gz import without a course.xml is
correct.
"""
with open(self.bad_tar) as btar:
resp = self.client.post(
self.url,
{
"name": self.bad_tar,
"course-data": [btar]
})
self.assertEquals(resp.status_code, 415)
def test_with_coursexml(self):
"""
Check that the response for a tar.gz import with a course.xml is
correct.
"""
with open(self.good_tar) as gtar:
resp = self.client.post(
self.url,
{
"name": self.good_tar,
"course-data": [gtar]
})
self.assertEquals(resp.status_code, 200)
......@@ -12,24 +12,26 @@ import copy
from django.contrib.auth.models import User
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.course_module import CourseDescriptor
from xmodule.exceptions import NotFoundError
from uuid import uuid4
from pymongo import MongoClient
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
"""
Tests that rely on the toy and test_import_course courses.
......@@ -58,6 +60,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=uname, password=password)
def tearDown(self):
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear()
def load_test_import_course(self):
'''
Load the standard course used to test imports (for do_import_static=False behavior).
......@@ -121,3 +127,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
self.assertIn('/static/', handouts.data)
def test_tab_name_imports_correctly(self):
module_store, content_store, course, course_location = self.load_test_import_course()
print "course tabs = {0}".format(course.tabs)
self.assertEqual(course.tabs[2]['name'],'Syllabus')
......@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
resp.content,
"application/json"
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
class TestCreateItem(CourseTestCase):
......
......@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase):
def test_view_index(self):
"Basic check that the textbook index page responds correctly"
resp = self.client.get(self.url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# we don't have resp.context right now,
# due to bugs in our testing harness :(
if resp.context:
......@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content)
self.assertEqual(self.course.pdf_textbooks, obj)
......@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content)
self.assertEqual(content, obj)
......@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# reload course
store = get_modulestore(self.course.location)
......@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
obj = json.loads(resp.content)
self.assertIn("error", obj)
......@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
self.assertNotIn("Location", resp)
......@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase):
def test_get_1(self):
"Get the first textbook"
resp = self.client.get(self.url1)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook1)
def test_get_2(self):
"Get the second textbook"
resp = self.client.get(self.url2)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook2)
......@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase):
def test_delete(self):
"Delete a textbook by ID"
resp = self.client.delete(self.url1)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
course = self.store.get_item(self.course.location)
self.assertEqual(course.pdf_textbooks, [self.textbook2])
......@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase):
)
self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(url)
self.assert2XX(resp2.status_code)
self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content)
self.assertEqual(compare, textbook)
course = self.store.get_item(self.course.location)
......@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase):
)
self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(self.url2)
self.assert2XX(resp2.status_code)
self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content)
self.assertEqual(compare, replacement)
course = self.store.get_item(self.course.location)
......
......@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase):
def test_detail_inactive(self):
resp = self.client.get(self.inactive_detail_url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
result = json.loads(resp.content)
self.assertFalse(result["active"])
def test_detail_invalid(self):
resp = self.client.get(self.invalid_detail_url)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 404)
result = json.loads(resp.content)
self.assertIn("error", result)
......@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
data={"role": None},
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
self.assert_not_enrolled()
......@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
self.assert_not_enrolled()
......@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "staff"},
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"},
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
......@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"},
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
......@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase):
})
resp = self.client.delete(self_url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
user = User.objects.get(email=self.user.email)
groups = [g.name for g in user.groups.all()]
......@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase):
self.ext_user.save()
resp = self.client.delete(self.detail_url)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
# reload user from DB
......@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
self.assert_enrolled()
def test_staff_to_instructor_still_enrolled(self):
......@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
self.assert_enrolled()
def assert_not_enrolled(self):
......
from django.test.client import Client
from django.test.utils import override_settings
from django.core.cache import cache
from django.core.urlresolvers import reverse
from .utils import parse_json, user, registration
from contentstore.tests.utils import parse_json, user, registration
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from contentstore.tests.modulestore_config import TEST_MODULESTORE
import datetime
from pytz import UTC
@override_settings(MODULESTORE=TEST_MODULESTORE)
class ContentStoreTestCase(ModuleStoreTestCase):
def _login(self, email, password):
"""
......
......@@ -7,9 +7,11 @@ import json
from student.models import Registration
from django.contrib.auth.models import User
from django.test.client import Client
from django.test.utils import override_settings
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from contentstore.tests.modulestore_config import TEST_MODULESTORE
def parse_json(response):
......@@ -27,6 +29,7 @@ def registration(email):
return Registration.objects.get(user__email=email)
@override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase):
def setUp(self):
"""
......
......@@ -10,6 +10,7 @@ from .component import *
from .course import *
from .error import *
from .item import *
from .import_export import *
from .preview import *
from .public import *
from .user import *
......
......@@ -4,6 +4,7 @@ import os
import tarfile
import shutil
import cgi
import re
from functools import partial
from tempfile import mkdtemp
from path import path
......@@ -35,9 +36,7 @@ from .access import get_location_and_verify_access
from util.json_request import JsonResponse
__all__ = ['asset_index', 'upload_asset', 'import_course',
'generate_export_course', 'export_course']
__all__ = ['asset_index', 'upload_asset']
def assets_to_json_dict(assets):
"""
......@@ -167,7 +166,7 @@ def upload_asset(request, org, course, coursename):
sc_partial = partial(StaticContent, content_loc, filename, mime_type)
if chunked:
content = sc_partial(upload_file.chunks())
temp_filepath = upload_file.temporary_file_path()
tempfile_path = upload_file.temporary_file_path()
else:
content = sc_partial(upload_file.read())
tempfile_path = None
......@@ -260,179 +259,3 @@ def remove_asset(request, org, course, name):
return HttpResponse()
@ensure_csrf_cookie
@require_http_methods(("GET", "POST", "PUT"))
@login_required
def import_course(request, org, course, name):
"""
This method will handle a POST request to upload and import a .tar.gz file into a specified course
"""
location = get_location_and_verify_access(request, org, course, name)
if request.method in ('POST', 'PUT'):
filename = request.FILES['course-data'].name
if not filename.endswith('.tar.gz'):
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
data_root = path(settings.GITHUB_REPO_ROOT)
course_subdir = "{0}-{1}-{2}".format(org, course, name)
course_dir = data_root / course_subdir
if not course_dir.isdir():
os.mkdir(course_dir)
temp_filepath = course_dir / filename
logging.debug('importing course to {0}'.format(temp_filepath))
# stream out the uploaded files in chunks to disk
temp_file = open(temp_filepath, 'wb+')
for chunk in request.FILES['course-data'].chunks():
temp_file.write(chunk)
temp_file.close()
tar_file = tarfile.open(temp_filepath)
tar_file.extractall(course_dir + '/')
# find the 'course.xml' file
dirpath = None
for dirpath, _dirnames, filenames in os.walk(course_dir):
for filename in filenames:
if filename == 'course.xml':
break
if filename == 'course.xml':
break
if filename != 'course.xml':
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
logging.debug('found course.xml at {0}'.format(dirpath))
if dirpath != course_dir:
for fname in os.listdir(dirpath):
shutil.move(dirpath / fname, course_dir)
_module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False,
static_content_store=contentstore(),
target_location_namespace=location,
draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
logging.debug('new course at {0}'.format(course_items[0].location))
create_all_course_groups(request.user, course_items[0].location)
logging.debug('created all course groups at {0}'.format(course_items[0].location))
return HttpResponse(json.dumps({'Status': 'OK'}))
else:
course_module = modulestore().get_item(location)
return render_to_response('import.html', {
'context_course': course_module,
'successful_import_redirect_url': reverse('course_index', kwargs={
'org': location.org,
'course': location.course,
'name': location.name,
})
})
@ensure_csrf_cookie
@login_required
def generate_export_course(request, org, course, name):
"""
This method will serialize out a course to a .tar.gz file which contains a XML-based representation of
the course
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_instance(location.course_id, location)
loc = Location(location)
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
root_dir = path(mkdtemp())
try:
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
except SerializationError, e:
logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
unit = None
failed_item = None
parent = None
try:
failed_item = modulestore().get_instance(course_module.location.course_id, e.location)
parent_locs = modulestore().get_parent_locations(failed_item.location, course_module.location.course_id)
if len(parent_locs) > 0:
parent = modulestore().get_item(parent_locs[0])
if parent.location.category == 'vertical':
unit = parent
except:
# if we have a nested exception, then we'll show the more generic error message
pass
return render_to_response('export.html', {
'context_course': course_module,
'successful_import_redirect_url': '',
'in_err': True,
'raw_err_msg': str(e),
'failed_module': failed_item,
'unit': unit,
'edit_unit_url': reverse('edit_unit', kwargs={
'location': parent.location
}) if parent else '',
'course_home_url': reverse('course_index', kwargs={
'org': org,
'course': course,
'name': name
})
})
except Exception, e:
logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
return render_to_response('export.html', {
'context_course': course_module,
'successful_import_redirect_url': '',
'in_err': True,
'unit': None,
'raw_err_msg': str(e),
'course_home_url': reverse('course_index', kwargs={
'org': org,
'course': course,
'name': name
})
})
logging.debug('tar file being generated at {0}'.format(export_file.name))
tar_file = tarfile.open(name=export_file.name, mode='w:gz')
tar_file.add(root_dir / name, arcname=name)
tar_file.close()
# remove temp dir
shutil.rmtree(root_dir / name)
wrapper = FileWrapper(export_file)
response = HttpResponse(wrapper, content_type='application/x-tgz')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
response['Content-Length'] = os.path.getsize(export_file.name)
return response
@ensure_csrf_cookie
@login_required
def export_course(request, org, course, name):
"""
This method serves up the 'Export Course' page
"""
location = get_location_and_verify_access(request, org, course, name)
course_module = modulestore().get_item(location)
return render_to_response('export.html', {
'context_course': course_module,
'successful_import_redirect_url': ''
})
......@@ -18,6 +18,7 @@ from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.exceptions import (
ItemNotFoundError, InvalidLocationError)
......@@ -206,7 +207,8 @@ def course_info(request, org, course, name, provided_id=None):
'context_course': course_module,
'url_base': "/" + org + "/" + course + "/",
'course_updates': json.dumps(get_course_updates(location)),
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() })
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url(),
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(location) + '/'})
@expect_json
......
......@@ -246,7 +246,7 @@ PIPELINE_JS = {
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/models/uploads.js', 'js/views/uploads.js',
'js/models/textbook.js', 'js/views/textbook.js',
'js/views/assets.js', 'js/utility.js',
'js/views/assets.js', 'js/src/utility.js',
'js/models/settings/course_grading_policy.js'],
'output_filename': 'js/cms-application.js',
'test_order': 0
......
"""
This configuration is used for running jasmine tests
"""
# We intentionally define lots of variables that aren't used, and
# want to import all variables from base settings files
# pylint: disable=W0401, W0614
from .test import *
from logsettings import get_logger_config
ENABLE_JASMINE = True
DEBUG = True
LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
dev_env=True,
debug=True,
local_loglevel='ERROR',
console_loglevel='ERROR')
PIPELINE_JS['js-test-source'] = {
'source_filenames': sum([
pipeline_group['source_filenames']
for group_name, pipeline_group
in sorted(PIPELINE_JS.items(), key=lambda item: item[1].get('test_order', 1e100))
if group_name != 'spec'
], []),
'output_filename': 'js/cms-test-source.js'
}
PIPELINE_JS['spec'] = {
'source_filenames': sorted(rooted_glob(PROJECT_ROOT / 'static/', 'coffee/spec/**/*.js')),
'output_filename': 'js/cms-spec.js'
}
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
JASMINE_REPORT_DIR = os.environ.get('JASMINE_REPORT_DIR', 'reports/cms/jasmine')
TEMPLATE_CONTEXT_PROCESSORS += ('settings_context_processor.context_processors.settings',)
TEMPLATE_VISIBLE_SETTINGS = ('JASMINE_REPORT_DIR', )
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/phantom-jasmine/lib')
STATICFILES_DIRS.append(REPO_ROOT/'node_modules/jasmine-reporters/src')
# Remove the localization middleware class because it requires the test database
# to be sync'd and migrated in order to run the jasmine tests interactively
# with a browser
MIDDLEWARE_CLASSES = tuple(e for e in MIDDLEWARE_CLASSES \
if e != 'django.middleware.locale.LocaleMiddleware')
INSTALLED_APPS += ('django_jasmine', 'settings_context_processor')
from dogapi import dog_http_api, dog_stats_api
from django.conf import settings
from xmodule.modulestore.django import modulestore
from django.dispatch import Signal
from request_cache.middleware import RequestCache
from django.core.cache import get_cache
CACHE = get_cache('mongo_metadata_inheritance')
for store_name in settings.MODULESTORE:
store = modulestore(store_name)
store.set_modulestore_configuration({
'metadata_inheritance_cache_subsystem': CACHE,
'request_cache': RequestCache.get_request_cache()
})
modulestore_update_signal = Signal(providing_args=['modulestore', 'course_id', 'location'])
store.modulestore_update_signal = modulestore_update_signal
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
"""
Module with code executed during Studio startup
"""
from django.conf import settings
# Force settings to run so that the python path is modified
settings.INSTALLED_APPS # pylint: disable=W0104
from django_startup import autostartup
# TODO: Remove this code once Studio/CMS runs via wsgi in all environments
INITIALIZED = False
def run():
"""
Executed during django startup
"""
global INITIALIZED
if INITIALIZED:
return
INITIALIZED = True
autostartup()
{
"static_files": [
"../jsi18n/",
"js/vendor/RequireJS.js",
"js/vendor/jquery.min.js",
"js/vendor/jquery-ui.min.js",
"js/vendor/jquery.ui.draggable.js",
"js/vendor/jquery.cookie.js",
"js/vendor/json2.js",
"js/vendor/underscore-min.js",
"js/vendor/underscore.string.min.js",
"js/vendor/backbone-min.js",
"js/vendor/backbone-associations-min.js",
"js/vendor/jquery.leanModal.min.js",
"js/vendor/jquery.form.js",
"js/vendor/sinon-1.7.1.js",
"js/vendor/jasmine-stealth.js",
"js/test/i18n.js"
]
}
../../templates/js/
\ No newline at end of file
../../../templates/js/edit-chapter.underscore
\ No newline at end of file
../../../templates/js/edit-textbook.underscore
\ No newline at end of file
../../../templates/js/metadata-editor.underscore
\ No newline at end of file
../../../templates/js/metadata-list-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-number-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-option-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-string-entry.underscore
\ No newline at end of file
../../../templates/js/no-textbooks.underscore
\ No newline at end of file
../../../templates/js/section-name-edit.underscore
\ No newline at end of file
../../../templates/js/show-textbook.underscore
\ No newline at end of file
../../../templates/js/system-feedback.underscore
\ No newline at end of file
../../../templates/js/upload-dialog.underscore
\ No newline at end of file
jasmine.getFixtures().fixturesPath = 'fixtures'
jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
# Stub jQuery.cookie
@stubCookies =
......
courseInfoPage = """
<div class="course-info-wrapper">
<div class="main-column window">
<article class="course-updates" id="course-update-view">
<ol class="update-list" id="course-update-list"></ol>
</article>
</div>
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
</div>
"""
commonSetup = () ->
window.analytics = jasmine.createSpyObj('analytics', ['track'])
window.course_location_analytics = jasmine.createSpy()
window.courseUpdatesXhr = sinon.useFakeXMLHttpRequest()
requests = []
window.courseUpdatesXhr.onCreate = (xhr) -> requests.push(xhr)
return requests
commonCleanup = () ->
window.courseUpdatesXhr.restore()
delete window.analytics
delete window.course_location_analytics
describe "Course Updates", ->
courseInfoTemplate = readFixtures('course_info_update.underscore')
beforeEach ->
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
appendSetFixtures courseInfoPage
@collection = new CMS.Models.CourseUpdateCollection()
@courseInfoEdit = new CMS.Views.ClassInfoUpdateView({
el: $('.course-updates'),
collection: @collection,
base_asset_url : 'base-asset-url/'
})
@courseInfoEdit.render()
@event = {
preventDefault : () -> 'no op'
}
@createNewUpdate = () ->
# Edit button is not in the template under test (it is in parent HTML).
# Therefore call onNew directly.
@courseInfoEdit.onNew(@event)
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
@courseInfoEdit.$el.find('.save-button').click()
@requests = commonSetup()
afterEach ->
commonCleanup()
it "does not rewrite links on save", ->
# Create a new update, verifying that the model is created
# in the collection and save is called.
expect(@collection.isEmpty()).toBeTruthy()
@courseInfoEdit.onNew(@event)
expect(@collection.length).toEqual(1)
model = @collection.at(0)
spyOn(model, "save").andCallThrough()
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
# Click the "Save button."
@courseInfoEdit.$el.find('.save-button').click()
expect(model.save).toHaveBeenCalled()
# Verify content sent to server does not have rewritten links.
contentSaved = JSON.parse(this.requests[0].requestBody).content
expect(contentSaved).toEqual('/static/image.jpg')
it "does rewrite links for preview", ->
# Create a new update.
@createNewUpdate()
# Verify the link is rewritten for preview purposes.
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
expect(previewContents).toEqual('base-asset-url/image.jpg')
it "shows static links in edit mode", ->
@createNewUpdate()
# Click edit and verify CodeMirror contents.
@courseInfoEdit.$el.find('.edit-button').click()
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
describe "Course Handouts", ->
handoutsTemplate = readFixtures('course_info_handouts.underscore')
beforeEach ->
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate))
appendSetFixtures courseInfoPage
@model = new CMS.Models.ModuleInfo({
id: 'handouts-id',
data: '/static/fromServer.jpg'
})
@handoutsEdit = new CMS.Views.ClassInfoHandoutsView({
el: $('#course-handouts-view'),
model: @model,
base_asset_url: 'base-asset-url/'
});
@handoutsEdit.render()
@requests = commonSetup()
afterEach ->
commonCleanup()
it "does not rewrite links on save", ->
# Enter something in the handouts section, verifying that the model is saved
# when "Save" is clicked.
@handoutsEdit.$el.find('.edit-button').click()
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
spyOn(@model, "save").andCallThrough()
@handoutsEdit.$el.find('.save-button').click()
expect(@model.save).toHaveBeenCalled()
contentSaved = JSON.parse(this.requests[0].requestBody).data
expect(contentSaved).toEqual('/static/image.jpg')
it "does rewrite links in initial content", ->
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/fromServer.jpg')
it "does rewrite links after edit", ->
# Edit handouts and save.
@handoutsEdit.$el.find('.edit-button').click()
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
@handoutsEdit.$el.find('.save-button').click()
# Verify preview text.
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/image.jpg')
it "shows static links in edit mode", ->
# Click edit and verify CodeMirror contents.
@handoutsEdit.$el.find('.edit-button').click()
expect(@handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg')
......@@ -64,20 +64,31 @@ class CMS.Views.TabsEdit extends Backbone.View
course: course_location_analytics
deleteTab: (event) =>
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.'
return
$component = $(event.currentTarget).parents('.component')
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
$.post('/delete_item', {
id: $component.data('id')
}, =>
$component.remove()
)
confirm = new CMS.Views.Prompt.Warning
title: gettext('Delete Component Confirmation')
message: gettext('Are you sure you want to delete this component? This action cannot be undone.')
actions:
primary:
text: gettext("OK")
click: (view) ->
view.hide()
$component = $(event.currentTarget).parents('.component')
analytics.track "Deleted Static Page",
course: course_location_analytics
id: $component.data('id')
deleting = new CMS.Views.Notification.Mini
title: gettext('Deleting') + '&hellip;'
deleting.show()
$.post('/delete_item', {
id: $component.data('id')
}, =>
$component.remove()
deleting.hide()
)
secondary: [
text: gettext('Cancel')
click: (view) ->
view.hide()
]
confirm.show()
......@@ -3,6 +3,26 @@
The render here adds views for each update/handout by delegating to their collections but does not
generate any html for the surrounding page.
*/
var editWithCodeMirror = function(model, contentName, baseAssetUrl, textArea) {
var content = rewriteStaticLinks(model.get(contentName), baseAssetUrl, '/static/');
model.set(contentName, content);
var $codeMirror = CodeMirror.fromTextArea(textArea, {
mode: "text/html",
lineNumbers: true,
lineWrapping: true
});
$codeMirror.setValue(content);
$codeMirror.clearHistory();
return $codeMirror;
};
var changeContentToPreview = function (model, contentName, baseAssetUrl) {
var content = rewriteStaticLinks(model.get(contentName), '/static/', baseAssetUrl);
model.set(contentName, content);
return content;
};
CMS.Views.CourseInfoEdit = Backbone.View.extend({
// takes CMS.Models.CourseInfo as model
tagName: 'div',
......@@ -11,18 +31,19 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
new CMS.Views.ClassInfoUpdateView({
el: $('body.updates'),
collection: this.model.get('updates')
collection: this.model.get('updates'),
base_asset_url: this.model.get('base_asset_url')
});
new CMS.Views.ClassInfoHandoutsView({
el: this.$('#course-handouts-view'),
model: this.model.get('handouts')
model: this.model.get('handouts'),
base_asset_url: this.model.get('base_asset_url')
});
return this;
}
});
// ??? Programming style question: should each of these classes be in separate files?
CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// collection is CourseUpdateCollection
events: {
......@@ -48,6 +69,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var self = this;
this.collection.each(function (update) {
try {
changeContentToPreview(update, 'content', self.options['base_asset_url'])
var newEle = self.template({ updateModel : update });
$(updateEle).append(newEle);
} catch (e) {
......@@ -72,20 +94,18 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$(updateEle).prepend($newForm);
var $textArea = $newForm.find(".new-update-content").first();
if (this.$codeMirror == null ) {
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
mode: "text/html",
lineNumbers: true,
lineWrapping: true
});
$newForm.addClass('editing');
this.$currentPost = $newForm.closest('li');
window.$modalCover.show();
window.$modalCover.bind('click', function() {
self.closeEditor(self, true);
self.closeEditor(true);
});
$('.date').datepicker('destroy');
......@@ -110,7 +130,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
ele.remove();
}
});
this.closeEditor(this);
this.closeEditor();
analytics.track('Saved Course Update', {
'course': course_location_analytics,
......@@ -122,8 +142,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
event.preventDefault();
// change editor contents back to model values and hide the editor
$(this.editor(event)).hide();
// If the model was never created (user created a new update, then pressed Cancel),
// we wish to remove it from the DOM.
var targetModel = this.eventModel(event);
this.closeEditor(this, !targetModel.id);
this.closeEditor(!targetModel.id);
},
onEdit: function(event) {
......@@ -134,16 +156,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$(this.editor(event)).show();
var $textArea = this.$currentPost.find(".new-update-content").first();
if (this.$codeMirror == null ) {
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
var targetModel = this.eventModel(event);
this.$codeMirror = editWithCodeMirror(targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
window.$modalCover.show();
var targetModel = this.eventModel(event);
window.$modalCover.bind('click', function() {
self.closeEditor(self);
});
......@@ -193,31 +209,35 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
}
});
confirm.show();
},
},
closeEditor: function(self, removePost) {
var targetModel = self.collection.get(self.$currentPost.attr('name'));
closeEditor: function(removePost) {
var targetModel = this.collection.get(this.$currentPost.attr('name'));
if(removePost) {
self.$currentPost.remove();
this.$currentPost.remove();
}
// close the modal and insert the appropriate data
self.$currentPost.removeClass('editing');
self.$currentPost.find('.date-display').html(targetModel.get('date'));
self.$currentPost.find('.date').val(targetModel.get('date'));
try {
// just in case the content causes an error (embedded js errors)
self.$currentPost.find('.update-contents').html(targetModel.get('content'));
self.$currentPost.find('.new-update-content').val(targetModel.get('content'));
} catch (e) {
// ignore but handle rest of page
else {
// close the modal and insert the appropriate data
this.$currentPost.removeClass('editing');
this.$currentPost.find('.date-display').html(targetModel.get('date'));
this.$currentPost.find('.date').val(targetModel.get('date'));
var content = changeContentToPreview(targetModel, 'content', this.options['base_asset_url'])
try {
// just in case the content causes an error (embedded js errors)
this.$currentPost.find('.update-contents').html(content);
this.$currentPost.find('.new-update-content').val(content);
} catch (e) {
// ignore but handle rest of page
}
this.$currentPost.find('form').hide();
this.$currentPost.find('.CodeMirror').remove();
}
self.$currentPost.find('form').hide();
window.$modalCover.unbind('click');
window.$modalCover.hide();
this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove();
},
// Dereferencing from events to screen elements
......@@ -275,8 +295,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
},
render: function () {
var updateEle = this.$el;
var self = this;
changeContentToPreview(this.model, 'data', this.options['base_asset_url'])
this.$el.html(
$(this.template( {
model: this.model
......@@ -295,22 +315,17 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
var self = this;
this.$editor.val(this.$preview.html());
this.$form.show();
if (this.$codeMirror == null) {
this.$codeMirror = CodeMirror.fromTextArea(this.$editor.get(0), {
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
this.$codeMirror = editWithCodeMirror(self.model, 'data', self.options['base_asset_url'], this.$editor.get(0));
window.$modalCover.show();
window.$modalCover.bind('click', function() {
self.closeEditor(self);
self.closeEditor();
});
},
onSave: function(event) {
this.model.set('data', this.$codeMirror.getValue());
this.render();
var saving = new CMS.Views.Notification.Mini({
title: gettext('Saving') + '&hellip;'
});
......@@ -320,8 +335,9 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
saving.hide();
}
});
this.render();
this.$form.hide();
this.closeEditor(this);
this.closeEditor();
analytics.track('Saved Course Handouts', {
'course': course_location_analytics
......@@ -331,14 +347,14 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
onCancel: function(event) {
this.$form.hide();
this.closeEditor(this);
this.closeEditor();
},
closeEditor: function(self) {
closeEditor: function() {
this.$form.hide();
window.$modalCover.unbind('click');
window.$modalCover.hide();
self.$form.find('.CodeMirror').remove();
this.$form.find('.CodeMirror').remove();
this.$codeMirror = null;
}
});
---
# JavaScript test suite description
#
#
# To run all the tests and print results to the console:
#
# js-test-tool run TEST_SUITE --use-firefox
#
# where `TEST_SUITE` is this file.
#
#
# To run the tests in your default browser ("dev mode"):
#
# js-test-tool dev TEST_SUITE
#
test_suite_name: cms
test_runner: jasmine
# Path prepended to source files in the coverage report (optional)
# For example, if the source path
# is "src/source.js" (relative to this YAML file)
# and the prepend path is "base/dir"
# then the coverage report will show
# "base/dir/src/source.js"
prepend_path: cms/static
# Paths to library JavaScript files (optional)
lib_paths:
- xmodule_js/common_static/coffee/src/ajax_prefix.js
- xmodule_js/common_static/coffee/src/logger.js
- xmodule_js/common_static/js/vendor/RequireJS.js
- xmodule_js/common_static/js/vendor/json2.js
- xmodule_js/common_static/js/vendor/jquery.min.js
- xmodule_js/common_static/js/vendor/jquery-ui.min.js
- xmodule_js/common_static/js/vendor/jquery.cookie.js
- xmodule_js/common_static/js/vendor/jquery.qtip.min.js
- xmodule_js/common_static/js/vendor/swfobject/swfobject.js
- xmodule_js/common_static/js/vendor/jquery.ba-bbq.min.js
- xmodule_js/common_static/js/vendor/annotator.min.js
- xmodule_js/common_static/js/vendor/annotator.store.min.js
- xmodule_js/common_static/js/vendor/annotator.tags.min.js
- xmodule_js/common_static/js/vendor/underscore-min.js
- xmodule_js/common_static/js/vendor/underscore.string.min.js
- xmodule_js/common_static/js/vendor/backbone-min.js
- xmodule_js/common_static/js/vendor/backbone-associations-min.js
- xmodule_js/common_static/js/vendor/timepicker/jquery.timepicker.js
- xmodule_js/common_static/js/vendor/jquery.leanModal.min.js
- xmodule_js/common_static/js/vendor/jquery.form.js
- xmodule_js/common_static/js/vendor/sinon-1.7.1.js
- xmodule_js/common_static/js/vendor/jasmine-jquery.js
- xmodule_js/common_static/js/vendor/jasmine-stealth.js
- xmodule_js/common_static/js/vendor/CodeMirror/codemirror.js
- xmodule_js/src/xmodule.js
- xmodule_js/src
- xmodule_js/common_static/js/test/add_ajax_prefix.js
- xmodule_js/common_static/js/src/utility.js
# Paths to source JavaScript files
src_paths:
- coffee/src
- js
# Paths to spec (test) JavaScript files
spec_paths:
- coffee/spec/helpers.js
- coffee/spec
# Paths to fixture files (optional)
# The fixture path will be set automatically when using jasmine-jquery.
# (https://github.com/velesin/jasmine-jquery)
#
# You can then access fixtures using paths relative to
# the test suite description:
#
# loadFixtures('path/to/fixture/fixture.html');
#
fixture_paths:
- coffee/fixtures
# Regular expressions used to exclude *.js files from
# appearing in the test runner page.
# Files are included by default, which means that they
# are loaded using a <script> tag in the test runner page.
# When loading many files, this can be slow, so
# exclude any files you don't need.
#exclude_from_page:
# - path/to/lib/exclude/*
# Regular expression used to guarantee that a *.js file
# is included in the test runner page.
# If a file name matches both `exclude_from_page` and
# `include_in_page`, the file WILL be included.
# You can use this to exclude all files in a directory,
# but make an exception for particular files.
#include_in_page:
# - path/to/lib/exclude/exception_*.js
......@@ -57,6 +57,11 @@ body.course.import {
color: $error-red;
}
.status-block {
display: none;
font-size: 13px;
}
.choose-file-button {
@include blue-button;
padding: 10px 50px 11px;
......
../../common/lib/xmodule/xmodule/js/
\ No newline at end of file
......@@ -39,6 +39,7 @@
model : new CMS.Models.CourseInfo({
courseId : '${context_course.location}',
updates : course_updates,
base_asset_url : '${base_asset_url}',
handouts : course_handouts
})
});
......
......@@ -25,13 +25,16 @@
<p>${_("File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a {filename} file.").format(filename='<code>course.xml</code>')}</p>
<p>${_("Please note that if your course has any problems with auto-generated {nodename} nodes, re-importing your course could cause the loss of student data associated with those problems.").format(nodename='<code>url_name</code>')}</p>
</div>
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
<form id="fileupload" method="post" enctype="multipart/form-data"
class="import-form" url="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}">
<h2>${_("Course to import:")}</h2>
<p class="error-block"></p>
<a href="#" class="choose-file-button">${_("Choose File")}</a>
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">${_("change")}</a></p>
<input type="file" name="course-data" class="file-input">
<input type="submit" value="${_('Replace my course with the one above')}" class="submit-button">
<input type="file" name="course-data" class="file-input" >
<input type="submit" value="${_('Replace my course with the one above')}" class="submit-button" >
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<p class="status-block">Unpacking...</p>
<div class="progress-bar">
<div class="progress-fill"></div>
<div class="percent">0%</div>
......@@ -43,6 +46,9 @@
</%block>
<%block name="jsextra">
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js')}"> </script>
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"> </script>
<script>
(function() {
......@@ -50,33 +56,61 @@ var bar = $('.progress-bar');
var fill = $('.progress-fill');
var percent = $('.percent');
var status = $('#status');
var statusBlock = $('.status-block');
var submitBtn = $('.submit-button');
$('form').ajaxForm({
beforeSend: function() {
status.empty();
var percentVal = '0%';
bar.show();
fill.width(percentVal);
percent.html(percentVal);
submitBtn.hide();
$('#fileupload').fileupload({
dataType: 'json',
type: 'POST',
maxChunkSize: 20 * 1000000, // 20 MB
autoUpload: false,
add: function(e, data) {
submitBtn.unbind('click');
var file = data.files[0];
if (file.type == "application/x-gzip") {
submitBtn.click(function(e){
e.preventDefault();
submitBtn.hide();
data.submit().complete(function(result, textStatus, xhr) {
if (result.status != 200) {
alert('${_("Your import has failed.")}\n\n' + JSON.parse(result.responseText)["ErrMsg"]);
submitBtn.show();
bar.hide();
} else {
if (result.responseText["ImportStatus"] == 1) {
bar.hide();
statusBlock.show();
}
}
});
});
} else {
data.files = [];
}
},
uploadProgress: function(event, position, total, percentComplete) {
var percentVal = percentComplete + '%';
progressall: function(e, data){
var percentVal = parseInt(data.loaded / data.total * 100, 10) + "%";
bar.show();
fill.width(percentVal);
percent.html(percentVal);
},
complete: function(xhr) {
if (xhr.status == 200) {
done: function(e, data){
bar.hide();
alert('${_("Your import was successful.")}');
window.location = '${successful_import_redirect_url}';
}
else
alert('${_("Your import has failed.")}\n\n' + xhr.responseText);
submitBtn.show();
bar.hide();
}
});
},
sequentialUploads: true
});
})();
</script>
</%block>
from django.conf import settings
from django.conf.urls import patterns, include, url
# Import this file so it can do its work, even though we don't use the name.
# pylint: disable=W0611
from . import one_time_startup
# TODO: This should be removed once the CMS is running via wsgi on all production servers
import cms.startup as startup
startup.run()
# There is a course creators admin table.
from ratelimitbackend import admin
......@@ -135,10 +135,6 @@ urlpatterns += (
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
)
if settings.ENABLE_JASMINE:
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
urlpatterns += (
url(r'^status/', include('service_status.urls')),
......
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.envs.aws")
import cms.startup as startup
startup.run()
# This application object is used by the development server
# as well as any WSGI server configured to use this file.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
......@@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts,
is_commentable_cohorted, get_cohort_by_name)
from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.tests.django_utils import xml_store_config
from xmodule.modulestore.tests.django_utils import mixed_store_config
# NOTE: running this with the lms.envs.test config works without
# manually overriding the modulestore. However, running with
# cms.envs.test doesn't.
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'}
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestCohorts(django.test.TestCase):
@staticmethod
......@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase):
"""
Make sure that course is reloaded every time--clear out the modulestore.
"""
# don't like this, but don't know a better way to undo all changes made
# to course. We don't have a course.clone() method.
_MODULESTORES.clear()
clear_existing_modulestores()
def test_get_cohort(self):
"""
......
......@@ -57,11 +57,6 @@ class CourseMode(models.Model):
def modes_for_course_dict(cls, course_id):
return { mode.slug : mode for mode in cls.modes_for_course(course_id) }
def __unicode__(self):
return u"{} : {}, min={}, prices={}".format(
self.course_id, self.mode_slug, self.min_price, self.suggested_prices
)
@classmethod
def mode_for_course(cls, course_id, mode_slug):
"""
......@@ -76,3 +71,8 @@ class CourseMode(models.Model):
return matched[0]
else:
return None
def __unicode__(self):
return u"{} : {}, min={}, prices={}".format(
self.course_id, self.mode_slug, self.min_price, self.suggested_prices
)
from django.conf import settings
from dogapi import dog_http_api, dog_stats_api
def run():
"""
Initialize connection to datadog during django startup.
Expects the datadog api key in the DATADOG_API settings key
"""
if hasattr(settings, 'DATADOG_API'):
dog_http_api.api_key = settings.DATADOG_API
dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
......@@ -14,11 +14,9 @@ from django.contrib.auth.models import AnonymousUser, User
from django.utils.importlib import import_module
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from xmodule.modulestore.django import editable_modulestore
from external_auth.models import ExternalAuthMap
from external_auth.views import shib_login, course_specific_login, course_specific_register
......@@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment
from student.models import UserProfile, Registration, CourseEnrollment
from student.tests.factories import UserFactory
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
# Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider'
# attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present
# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
......@@ -64,7 +64,7 @@ def gen_all_identities():
yield _build_identity_dict(mail, given_name, surname)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
class ShibSPTest(ModuleStoreTestCase):
"""
Tests for the Shibboleth SP, which communicates via request.META
......@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase):
request_factory = RequestFactory()
def setUp(self):
self.store = modulestore()
self.store = editable_modulestore()
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
def test_exception_shib_login(self):
......
......@@ -12,36 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from mako.lookup import TemplateLookup
import tempdir
from django.template import RequestContext
from django.conf import settings
requestcontext = None
lookup = {}
class MakoMiddleware(object):
def __init__(self):
"""Setup mako variables and lookup object"""
# Set all mako variables based on django settings
template_locations = settings.MAKO_TEMPLATES
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
if module_directory is None:
module_directory = tempdir.mkdtemp_clean()
for location in template_locations:
lookup[location] = TemplateLookup(directories=template_locations[location],
module_directory=module_directory,
output_encoding='utf-8',
input_encoding='utf-8',
default_filters=['decode.utf8'],
encoding_errors='replace',
)
import mitxmako
mitxmako.lookup = lookup
def process_request(self, request):
global requestcontext
......
......@@ -16,7 +16,7 @@ from django.template import Context
from django.http import HttpResponse
import logging
from . import middleware
import mitxmako
from django.conf import settings
from django.core.urlresolvers import reverse
log = logging.getLogger(__name__)
......@@ -80,15 +80,15 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_instance['marketing_link'] = marketing_link
# In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None:
for d in middleware.requestcontext:
if mitxmako.middleware.requestcontext is not None:
for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
if context:
context_dictionary.update(context)
# fetch and render template
template = middleware.lookup[namespace].get_template(template_name)
template = mitxmako.lookup[namespace].get_template(template_name)
return template.render_unicode(**context_dictionary)
......
"""
Initialize the mako template lookup
"""
import tempdir
from django.conf import settings
from mako.lookup import TemplateLookup
import mitxmako
def run():
"""Setup mako variables and lookup object"""
# Set all mako variables based on django settings
template_locations = settings.MAKO_TEMPLATES
module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
if module_directory is None:
module_directory = tempdir.mkdtemp_clean()
lookup = {}
for location in template_locations:
lookup[location] = TemplateLookup(
directories=template_locations[location],
module_directory=module_directory,
output_encoding='utf-8',
input_encoding='utf-8',
default_filters=['decode.utf8'],
encoding_errors='replace',
)
mitxmako.lookup = lookup
......@@ -16,7 +16,8 @@ from django.conf import settings
from mako.template import Template as MakoTemplate
from mitxmako.shortcuts import marketing_link
from mitxmako import middleware
import mitxmako
import mitxmako.middleware
django_variables = ['lookup', 'output_encoding', 'encoding_errors']
......@@ -33,7 +34,7 @@ class Template(MakoTemplate):
def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False):
overrides = dict([(k, getattr(middleware, k, None),) for k in django_variables])
overrides = dict([(k, getattr(mitxmako, k, None),) for k in django_variables])
overrides['lookup'] = overrides['lookup']['main']
kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs)
......@@ -47,8 +48,8 @@ class Template(MakoTemplate):
context_dictionary = {}
# In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None:
for d in middleware.requestcontext:
if mitxmako.middleware.requestcontext is not None:
for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
......
import re
from nose.tools import assert_equals, assert_true, assert_false
from nose.tools import assert_equals, assert_true, assert_false # pylint: disable=E0611
from static_replace import (replace_static_urls, replace_course_urls,
_url_replace_regex)
from mock import patch, Mock
......
......@@ -16,10 +16,6 @@ from django.contrib.auth.models import User
from student.models import UserProfile
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
class Command(BaseCommand):
help = \
......
......@@ -12,10 +12,6 @@ from django.contrib.auth.models import User
from student.models import UserProfile
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
def import_user(u):
user_info = u['u']
......
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
from student.models import UserTestGroup
import random
......@@ -11,8 +10,6 @@ import datetime
import json
from pytz import UTC
middleware.MakoMiddleware()
def group_from_value(groups, v):
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
......
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
class Command(BaseCommand):
help = \
......
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
import mitxmako
class Command(BaseCommand):
......@@ -17,8 +15,8 @@ body, and an _subject.txt for the subject. '''
#text = open(args[0]).read()
#subject = open(args[1]).read()
users = User.objects.all()
text = middleware.lookup['main'].get_template('email/' + args[0] + ".txt").render()
subject = middleware.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
text = mitxmako.lookup['main'].get_template('email/' + args[0] + ".txt").render()
subject = mitxmako.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
for user in users:
if user.is_active:
user.email_user(subject, text)
......@@ -4,15 +4,13 @@ import time
from django.core.management.base import BaseCommand
from django.conf import settings
import mitxmako.middleware as middleware
import mitxmako
from django.core.mail import send_mass_mail
import sys
import datetime
middleware.MakoMiddleware()
def chunks(l, n):
""" Yield successive n-sized chunks from l.
......@@ -41,8 +39,8 @@ rate -- messages per second
users = [u.strip() for u in open(user_file).readlines()]
message = middleware.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
subject = middleware.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
message = mitxmako.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
subject = mitxmako.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
rate = int(ratestr)
self.log_file = open(logfilename, "a+", buffering=0)
......
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
import mitxmako.middleware as middleware
import json
from student.models import UserProfile
middleware.MakoMiddleware()
class Command(BaseCommand):
help = \
......
......@@ -805,7 +805,8 @@ class CourseEnrollment(models.Model):
record.is_active = False
record.save()
except cls.DoesNotExist:
log.error("Tried to unenroll student {} from {} but they were not enrolled")
err_msg = u"Tried to unenroll student {} from {} but they were not enrolled"
log.error(err_msg.format(user, course_id))
@classmethod
def unenroll_by_email(cls, email, course_id):
......
"""
Student Views
"""
import datetime
import json
import logging
......@@ -52,6 +55,10 @@ from courseware.access import has_access
from external_auth.models import ExternalAuthMap
from bulk_email.models import Optout
import track.views
from statsd import statsd
from pytz import UTC
......@@ -62,8 +69,7 @@ Article = namedtuple('Article', 'title url author image deck publication publish
def csrf_token(context):
''' A csrf token that can be included in a form.
'''
"""A csrf token that can be included in a form."""
csrf_token = context.get('csrf_token', '')
if csrf_token == 'NOTPROVIDED':
return ''
......@@ -76,12 +82,12 @@ def csrf_token(context):
# This means that it should always return the same thing for anon
# users. (in particular, no switching based on query params allowed)
def index(request, extra_context={}, user=None):
'''
"""
Render the edX main page.
extra_context is used to allow immediate display of certain modal windows, eg signup,
as used by external_auth.
'''
"""
# The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
......@@ -265,6 +271,8 @@ def dashboard(request):
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
message = ""
if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
......@@ -292,6 +300,7 @@ def dashboard(request):
pass
context = {'courses': courses,
'course_optouts': course_optouts,
'message': message,
'external_auth_map': external_auth_map,
'staff_access': staff_access,
......@@ -411,7 +420,7 @@ def accounts_login(request, error=""):
# Need different levels of logging
@ensure_csrf_cookie
def login_user(request, error=""):
''' AJAX request to log in the user. '''
"""AJAX request to log in the user."""
if 'email' not in request.POST or 'password' not in request.POST:
return HttpResponse(json.dumps({'success': False,
'value': _('There was an error receiving your login information. Please email us.')})) # TODO: User error message
......@@ -494,11 +503,11 @@ def login_user(request, error=""):
@ensure_csrf_cookie
def logout_user(request):
'''
"""
HTTP request to log out the user. Redirects to marketing page.
Deletes both the CSRF and sessionid cookies so the marketing
site can determine the logged in state of the user
'''
"""
# We do not log here, because we have a handler registered
# to perform logging on successful logouts.
logout(request)
......@@ -512,8 +521,7 @@ def logout_user(request):
@login_required
@ensure_csrf_cookie
def change_setting(request):
''' JSON call to change a profile setting: Right now, location
'''
"""JSON call to change a profile setting: Right now, location"""
# TODO (vshnayder): location is no longer used
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
if 'location' in request.POST:
......@@ -581,10 +589,10 @@ def _do_create_account(post_vars):
@ensure_csrf_cookie
def create_account(request, post_override=None):
'''
"""
JSON call to create new edX account.
Used by form in signup_modal.html, which is included into navigation.html
'''
"""
js = {'success': False}
post_vars = post_override if post_override else request.POST
......@@ -818,10 +826,10 @@ def begin_exam_registration(request, course_id):
@ensure_csrf_cookie
def create_exam_registration(request, post_override=None):
'''
"""
JSON call to create a test center exam registration.
Called by form in test_center_register.html
'''
"""
post_vars = post_override if post_override else request.POST
# first determine if we need to create a new TestCenterUser, or if we are making any update
......@@ -974,8 +982,7 @@ def auto_auth(request):
@ensure_csrf_cookie
def activate_account(request, key):
''' When link in activation e-mail is clicked
'''
"""When link in activation e-mail is clicked"""
r = Registration.objects.filter(activation_key=key)
if len(r) == 1:
user_logged_in = request.user.is_authenticated()
......@@ -1010,7 +1017,7 @@ def activate_account(request, key):
@ensure_csrf_cookie
def password_reset(request):
''' Attempts to send a password reset e-mail. '''
""" Attempts to send a password reset e-mail. """
if request.method != "POST":
raise Http404
......@@ -1032,9 +1039,9 @@ def password_reset_confirm_wrapper(
uidb36=None,
token=None,
):
''' A wrapper around django.contrib.auth.views.password_reset_confirm.
""" A wrapper around django.contrib.auth.views.password_reset_confirm.
Needed because we want to set the user as active at this step.
'''
"""
# cribbed from django.contrib.auth.views.password_reset_confirm
try:
uid_int = base36_to_int(uidb36)
......@@ -1076,8 +1083,8 @@ def reactivation_email_for_user(user):
@ensure_csrf_cookie
def change_email_request(request):
''' AJAX call from the profile page. User wants a new e-mail.
'''
""" AJAX call from the profile page. User wants a new e-mail.
"""
## Make sure it checks for existing e-mail conflicts
if not request.user.is_authenticated:
raise Http404
......@@ -1132,9 +1139,9 @@ def change_email_request(request):
@ensure_csrf_cookie
@transaction.commit_manually
def confirm_email_change(request, key):
''' User requested a new e-mail. This is called when the activation
""" User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update
'''
"""
try:
try:
pec = PendingEmailChange.objects.get(activation_key=key)
......@@ -1191,7 +1198,7 @@ def confirm_email_change(request, key):
@ensure_csrf_cookie
def change_name_request(request):
''' Log a request for a new name. '''
""" Log a request for a new name. """
if not request.user.is_authenticated:
raise Http404
......@@ -1215,7 +1222,7 @@ def change_name_request(request):
@ensure_csrf_cookie
def pending_name_changes(request):
''' Web page which allows staff to approve or reject name changes. '''
""" Web page which allows staff to approve or reject name changes. """
if not request.user.is_staff:
raise Http404
......@@ -1231,7 +1238,7 @@ def pending_name_changes(request):
@ensure_csrf_cookie
def reject_name_change(request):
''' JSON: Name change process. Course staff clicks 'reject' on a given name change '''
""" JSON: Name change process. Course staff clicks 'reject' on a given name change """
if not request.user.is_staff:
raise Http404
......@@ -1269,13 +1276,36 @@ def accept_name_change_by_id(id):
@ensure_csrf_cookie
def accept_name_change(request):
''' JSON: Name change process. Course staff clicks 'accept' on a given name change
""" JSON: Name change process. Course staff clicks 'accept' on a given name change
We used this during the prototype but now we simply record name changes instead
of manually approving them. Still keeping this around in case we want to go
back to this approval method.
'''
"""
if not request.user.is_staff:
raise Http404
return accept_name_change_by_id(int(request.POST['id']))
@require_POST
@login_required
@ensure_csrf_cookie
def change_email_settings(request):
"""Modify logged-in user's setting for receiving emails from a course."""
user = request.user
course_id = request.POST.get("course_id")
receive_emails = request.POST.get("receive_emails")
if receive_emails:
optout_object = Optout.objects.filter(user=user, course_id=course_id)
if optout_object:
optout_object.delete()
log.info(u"User {0} ({1}) opted in to receive emails from course {2}".format(user.username, user.email, course_id))
track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard')
else:
Optout.objects.get_or_create(user=user, course_id=course_id)
log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id))
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
return HttpResponse(json.dumps({'success': True}))
......@@ -16,11 +16,6 @@ from requests import put
from base64 import encodestring
from json import dumps
# Let the LMS and CMS do their one-time setup
# For example, setting up mongo caches
# These names aren't used, but do important work on import.
from lms import one_time_startup # pylint: disable=W0611
from cms import one_time_startup # pylint: disable=W0611
from pymongo import MongoClient
import xmodule.modulestore.django
from xmodule.contentstore.django import _CONTENTSTORE
......@@ -161,9 +156,10 @@ def reset_databases(scenario):
mongo = MongoClient()
mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear()
modulestore = xmodule.modulestore.django.modulestore()
modulestore = xmodule.modulestore.django.editable_modulestore()
modulestore.collection.drop()
xmodule.modulestore.django._MODULESTORES.clear()
xmodule.modulestore.django.clear_existing_modulestores()
# Uncomment below to trigger a screenshot on error
......
......@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import editable_modulestore
from xmodule.contentstore.django import contentstore
from urllib import quote_plus
......@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False):
@world.absorb
def clear_courses():
# Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
modulestore().collection.drop()
editable_modulestore().collection.drop()
contentstore().fs_files.drop()
......@@ -15,7 +15,7 @@ from lettuce import world, step
from .course_helpers import *
from .ui_helpers import *
from lettuce.django import django_url
from nose.tools import assert_equals
from nose.tools import assert_equals # pylint: disable=E0611
from logging import getLogger
logger = getLogger(__name__)
......
......@@ -10,7 +10,7 @@ from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from lettuce.django import django_url
from nose.tools import assert_true
from nose.tools import assert_true # pylint: disable=E0611
@world.absorb
......
"""
Ideally, we wouldn't need to pull in all the calc symbols here,
but courses were using 'import calc', so we need this for
backwards compatibility
"""
from calc import *
......@@ -9,7 +9,7 @@ import operator
import numbers
import numpy
import scipy.constants
import calcfunctions
import functions
from pyparsing import (
Word, Literal, CaselessLiteral, ZeroOrMore, MatchFirst, Optional, Forward,
......@@ -20,9 +20,9 @@ DEFAULT_FUNCTIONS = {
'sin': numpy.sin,
'cos': numpy.cos,
'tan': numpy.tan,
'sec': calcfunctions.sec,
'csc': calcfunctions.csc,
'cot': calcfunctions.cot,
'sec': functions.sec,
'csc': functions.csc,
'cot': functions.cot,
'sqrt': numpy.sqrt,
'log10': numpy.log10,
'log2': numpy.log2,
......@@ -31,24 +31,24 @@ DEFAULT_FUNCTIONS = {
'arccos': numpy.arccos,
'arcsin': numpy.arcsin,
'arctan': numpy.arctan,
'arcsec': calcfunctions.arcsec,
'arccsc': calcfunctions.arccsc,
'arccot': calcfunctions.arccot,
'arcsec': functions.arcsec,
'arccsc': functions.arccsc,
'arccot': functions.arccot,
'abs': numpy.abs,
'fact': math.factorial,
'factorial': math.factorial,
'sinh': numpy.sinh,
'cosh': numpy.cosh,
'tanh': numpy.tanh,
'sech': calcfunctions.sech,
'csch': calcfunctions.csch,
'coth': calcfunctions.coth,
'sech': functions.sech,
'csch': functions.csch,
'coth': functions.coth,
'arcsinh': numpy.arcsinh,
'arccosh': numpy.arccosh,
'arctanh': numpy.arctanh,
'arcsech': calcfunctions.arcsech,
'arccsch': calcfunctions.arccsch,
'arccoth': calcfunctions.arccoth
'arcsech': functions.arcsech,
'arccsch': functions.arccsch,
'arccoth': functions.arccoth
}
DEFAULT_VARIABLES = {
'i': numpy.complex(0, 1),
......
......@@ -4,7 +4,7 @@ Unit tests for preview.py
"""
import unittest
import preview
from calc import preview
import pyparsing
......
......@@ -2,8 +2,8 @@ from setuptools import setup
setup(
name="calc",
version="0.1.1",
py_modules=["calc"],
version="0.2",
packages=["calc"],
install_requires=[
"pyparsing==1.5.6",
"numpy",
......
......@@ -555,6 +555,13 @@ class LoncapaProblem(object):
Used by get_html.
'''
if not isinstance(problemtree.tag, basestring):
# Comment and ProcessingInstruction nodes are not Elements,
# and we're ok leaving those behind.
# BTW: etree gives us no good way to distinguish these things
# other than to examine .tag to see if it's a string. :(
return
if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact.
......
......@@ -49,7 +49,7 @@ import pyparsing
from .registry import TagRegistry
from chem import chemcalc
from preview import latex_preview
from calc.preview import latex_preview
import xqueue_interface
from datetime import datetime
......
......@@ -915,7 +915,26 @@ class NumericalResponse(LoncapaResponse):
else:
return CorrectMap(self.answer_id, 'incorrect')
# TODO: add check_hint_condition(self, hxml_set, student_answers)
def compare_answer(self, ans1, ans2):
"""
Outside-facing function that lets us compare two numerical answers,
with this problem's tolerance.
"""
return compare_with_tolerance(
evaluator({}, {}, ans1),
evaluator({}, {}, ans2),
self.tolerance
)
def validate_answer(self, answer):
"""
Returns whether this answer is in a valid form.
"""
try:
evaluator(dict(), dict(), answer)
return True
except (StudentInputError, UndefinedVariable):
return False
def get_answers(self):
return {self.answer_id: self.correct_answer}
......@@ -1778,46 +1797,24 @@ class FormulaResponse(LoncapaResponse):
self.correct_answer, given, self.samples)
return CorrectMap(self.answer_id, correctness)
def check_formula(self, expected, given, samples):
variables = samples.split('@')[0].split(',')
numsamples = int(samples.split('@')[1].split('#')[1])
sranges = zip(*map(lambda x: map(float, x.split(",")),
samples.split('@')[1].split('#')[0].split(':')))
ranges = dict(zip(variables, sranges))
for _ in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context))
student_variables = {}
# ranges give numerical ranges for testing
for var in ranges:
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value
student_variables[str(var)] = value
# log.debug('formula: instructor_vars=%s, expected=%s' %
# (instructor_variables,expected))
# Call `evaluator` on the instructor's answer and get a number
instructor_result = evaluator(
instructor_variables, {},
expected, case_sensitive=self.case_sensitive
)
def tupleize_answers(self, answer, var_dict_list):
"""
Takes in an answer and a list of dictionaries mapping variables to values.
Each dictionary represents a test case for the answer.
Returns a tuple of formula evaluation results.
"""
out = []
for var_dict in var_dict_list:
try:
# log.debug('formula: student_vars=%s, given=%s' %
# (student_variables,given))
# Call `evaluator` on the student's answer; look for exceptions
student_result = evaluator(
student_variables,
{},
given,
case_sensitive=self.case_sensitive
)
out.append(evaluator(
var_dict,
dict(),
answer,
case_sensitive=self.case_sensitive,
))
except UndefinedVariable as uv:
log.debug(
'formularesponse: undefined variable in given=%s',
given
)
'formularesponse: undefined variable in formula=%s' % answer)
raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer"
)
......@@ -1840,17 +1837,70 @@ class FormulaResponse(LoncapaResponse):
# If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve))
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given))
cgi.escape(answer))
except Exception as err:
# traceback.print_exc()
log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given))
cgi.escape(answer))
return out
# No errors in student's response--actually test for correctness
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return "incorrect"
return "correct"
def randomize_variables(self, samples):
"""
Returns a list of dictionaries mapping variables to random values in range,
as expected by tupleize_answers.
"""
variables = samples.split('@')[0].split(',')
numsamples = int(samples.split('@')[1].split('#')[1])
sranges = zip(*map(lambda x: map(float, x.split(",")),
samples.split('@')[1].split('#')[0].split(':')))
ranges = dict(zip(variables, sranges))
out = []
for i in range(numsamples):
var_dict = {}
# ranges give numerical ranges for testing
for var in ranges:
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
value = random.uniform(*ranges[var])
var_dict[str(var)] = value
out.append(var_dict)
return out
def check_formula(self, expected, given, samples):
"""
Given an expected answer string, a given (student-produced) answer
string, and a samples string, return whether the given answer is
"correct" or "incorrect".
"""
var_dict_list = self.randomize_variables(samples)
student_result = self.tupleize_answers(given, var_dict_list)
instructor_result = self.tupleize_answers(expected, var_dict_list)
correct = all(compare_with_tolerance(student, instructor, self.tolerance)
for student, instructor in zip(student_result, instructor_result))
if correct:
return "correct"
else:
return "incorrect"
def compare_answer(self, ans1, ans2):
"""
An external interface for comparing whether a and b are equal.
"""
internal_result = self.check_formula(ans1, ans2, self.samples)
return internal_result == "correct"
def validate_answer(self, answer):
"""
Returns whether this answer is in a valid form.
"""
var_dict_list = self.randomize_variables(self.samples)
try:
self.tupleize_answers(answer, var_dict_list)
return True
except StudentInputError:
return False
def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word
......
......@@ -16,11 +16,11 @@ __ 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`::
need to install the requirements from requirements/edx-sandbox::
$ sudo pip install -r pre-sandbox-requirements.txt
$ sudo pip install -r sandbox-requirements.txt
$ pip install -r requirements/edx-sandbox/base.txt
$ pip install -r requirements/edx-sandbox/local.txt
$ pip install -r requirements/edx-sandbox/post.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
......
This diff is collapsed. Click to expand it.
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