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 ...@@ -46,3 +46,4 @@ autodeploy.properties
.ws_migrations_complete .ws_migrations_complete
.vagrant/ .vagrant/
logs logs
.testids/
[pep8] [pep8]
ignore=E501 ignore=E501
\ No newline at end of file exclude=migrations
\ No newline at end of file
...@@ -84,3 +84,5 @@ Mukul Goyal <miki@edx.org> ...@@ -84,3 +84,5 @@ Mukul Goyal <miki@edx.org>
Robert Marks <rmarks@edx.org> Robert Marks <rmarks@edx.org>
Yarko Tymciurak <yarkot1@gmail.com> Yarko Tymciurak <yarkot1@gmail.com>
Miles Steele <miles@milessteele.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, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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 Studio: Allow course authors to set their course image on the schedule
and details page, with support for JPEG and PNG images. 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 Blades: Took videoalpha out of alpha, replacing the old video player
Common: Allow instructors to input complicated expressions as answers to 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 ...@@ -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 to `enroll()`, `unenroll()`, and to check `is_enrolled()`, instead of creating
CourseEnrollment objects or querying them directly. 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 Studio: Email will be sent to admin address when a user requests course creator
privileges for Studio (edge only). privileges for Studio (edge only).
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step 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 from common import type_in_codemirror, press_the_notification_button
KEY_CSS = '.key input.policy-key' KEY_CSS = '.key input.policy-key'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step 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 terrain.steps import reload_the_page
from selenium.common.exceptions import StaleElementReferenceException from selenium.common.exceptions import StaleElementReferenceException
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# pylint: disable=W0621 # pylint: disable=W0621
from lettuce import world, step 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 auth.authz import get_user_by_email, get_course_groupname_for_role
from django.conf import settings from django.conf import settings
...@@ -265,9 +265,8 @@ def type_in_codemirror(index, text): ...@@ -265,9 +265,8 @@ def type_in_codemirror(index, text):
def upload_file(filename): 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) 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' button_css = '.upload-dialog .action-upload'
world.css_click(button_css) world.css_click(button_css)
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=W0621 #pylint: disable=W0621
from lettuce import world, step 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' DATA_LOCATION = 'i4x://edx/templates'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=C0111 #pylint: disable=C0111
from lettuce import world 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 from terrain.steps import reload_the_page
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
from lettuce import world, step from lettuce import world, step
from common import * 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 from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
......
...@@ -7,7 +7,7 @@ from selenium.webdriver.common.keys import Keys ...@@ -7,7 +7,7 @@ from selenium.webdriver.common.keys import Keys
from common import type_in_codemirror, upload_file from common import type_in_codemirror, upload_file
from django.conf import settings 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 TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
...@@ -168,15 +168,18 @@ def i_see_new_course_image(_step): ...@@ -168,15 +168,18 @@ def i_see_new_course_image(_step):
img = images[0] img = images[0]
expected_src = '/c4x/MITx/999/asset/image.jpg' expected_src = '/c4x/MITx/999/asset/image.jpg'
# Don't worry about the domain in the URL # 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') @step('the image URL should be present in the field')
def image_url_present(_step): def image_url_present(_step):
field_css = '#course-image-url' field_css = '#course-image-url'
field = world.css_find(field_css).first
expected_value = '/c4x/MITx/999/asset/image.jpg' expected_value = '/c4x/MITx/999/asset/image.jpg'
assert field.value == expected_value assert world.css_value(field_css) == expected_value
############### HELPER METHODS #################### ############### HELPER METHODS ####################
......
...@@ -5,7 +5,7 @@ from lettuce import world, step ...@@ -5,7 +5,7 @@ from lettuce import world, step
from common import create_studio_user from common import create_studio_user
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from auth.authz import get_course_groupname_for_role, get_user_by_email 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' PASSWORD = 'test'
EMAIL_EXTENSION = '@edx.org' EMAIL_EXTENSION = '@edx.org'
......
...@@ -45,3 +45,25 @@ Feature: Course updates ...@@ -45,3 +45,25 @@ Feature: Course updates
When I modify the handout to "<ol>Test</ol>" When I modify the handout to "<ol>Test</ol>"
Then I see the handout "Test" Then I see the handout "Test"
And I see a "saving" notification 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): ...@@ -38,6 +38,16 @@ def modify_update(_step, text):
change_text(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$') @step(u'I delete the update$')
def click_button(_step): def click_button(_step):
button_css = 'div.post-preview a.delete-button' button_css = 'div.post-preview a.delete-button'
...@@ -80,3 +90,10 @@ def change_text(text): ...@@ -80,3 +90,10 @@ def change_text(text):
type_in_codemirror(0, text) type_in_codemirror(0, text)
save_css = 'a.save-button' save_css = 'a.save-button'
world.css_click(save_css) 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 @@ ...@@ -2,7 +2,7 @@
#pylint: disable=C0111 #pylint: disable=C0111
from lettuce import world, step 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 from common import type_in_codemirror
DISPLAY_NAME = "Display Name" DISPLAY_NAME = "Display Name"
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal # pylint: disable=E0611
############### ACTIONS #################### ############### ACTIONS ####################
......
...@@ -11,8 +11,9 @@ Feature: Static Pages ...@@ -11,8 +11,9 @@ Feature: Static Pages
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the static pages page And I go to the static pages page
And I add a new page And I add a new page
When I will confirm all alerts
And I "delete" the "Empty" page 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 Then I should not see a "Empty" static page
# Safari won't update the name properly # Safari won't update the name properly
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
from lettuce import world, step from lettuce import world, step
from common import * from common import *
from nose.tools import assert_equal from nose.tools import assert_equal # pylint: disable=E0611
############### ACTIONS #################### ############### ACTIONS ####################
......
...@@ -29,7 +29,7 @@ def correct_video_settings(_step): ...@@ -29,7 +29,7 @@ def correct_video_settings(_step):
['Download Track', '', False], ['Download Track', '', False],
['Download Video', '', False], ['Download Video', '', False],
['End Time', '0', False], ['End Time', '0', False],
['HTML5 Subtitles', '', False], ['HTML5 Timed Transcript', '', False],
['Show Captions', 'True', False], ['Show Captions', 'True', False],
['Start Time', '0', False], ['Start Time', '0', False],
['Video Sources', '', False], ['Video Sources', '', False],
......
...@@ -3,11 +3,6 @@ from xmodule.modulestore.django import modulestore ...@@ -3,11 +3,6 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import check_module_metadata_editability from xmodule.modulestore.xml_importer import check_module_metadata_editability
from xmodule.course_module import CourseDescriptor 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): class Command(BaseCommand):
help = '''Enumerates through the course and find common errors''' help = '''Enumerates through the course and find common errors'''
...@@ -21,12 +16,6 @@ class Command(BaseCommand): ...@@ -21,12 +16,6 @@ class Command(BaseCommand):
loc = CourseDescriptor.id_to_location(loc_str) loc = CourseDescriptor.id_to_location(loc_str)
store = modulestore() 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) course = store.get_item(loc, depth=3)
err_cnt = 0 err_cnt = 0
......
...@@ -9,14 +9,10 @@ from xmodule.course_module import CourseDescriptor ...@@ -9,14 +9,10 @@ from xmodule.course_module import CourseDescriptor
from auth.authz import _copy_course_group 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 # 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): class Command(BaseCommand):
"""Clone a MongoDB-backed course to another location""" """Clone a MongoDB-backed course to another location"""
help = 'Clone a MongoDB backed course to another location' help = 'Clone a MongoDB backed course to another location'
...@@ -32,11 +28,6 @@ class Command(BaseCommand): ...@@ -32,11 +28,6 @@ class Command(BaseCommand):
mstore = modulestore('direct') mstore = modulestore('direct')
cstore = contentstore() 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("/") org, course_num, run = dest_course_id.split("/")
mstore.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num)) mstore.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num))
......
...@@ -9,14 +9,11 @@ from xmodule.course_module import CourseDescriptor ...@@ -9,14 +9,11 @@ from xmodule.course_module import CourseDescriptor
from .prompt import query_yes_no from .prompt import query_yes_no
from auth.authz import _delete_course_group 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 # To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
# #
CACHE = get_cache('mongo_metadata_inheritance')
class Command(BaseCommand): class Command(BaseCommand):
help = '''Delete a MongoDB backed course''' help = '''Delete a MongoDB backed course'''
...@@ -36,11 +33,6 @@ class Command(BaseCommand): ...@@ -36,11 +33,6 @@ class Command(BaseCommand):
ms = modulestore('direct') ms = modulestore('direct')
cs = contentstore() cs = contentstore()
ms.set_modulestore_configuration({
'metadata_inheritance_cache_subsystem': CACHE,
'request_cache': RequestCache.get_request_cache()
})
org, course_num, run = course_id.split("/") org, course_num, run = course_id.split("/")
ms.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num)) 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): ...@@ -60,11 +60,11 @@ class UploadTestCase(CourseTestCase):
f = BytesIO("sample content") f = BytesIO("sample content")
f.name = "sample.txt" f.name = "sample.txt"
resp = self.client.post(self.url, {"name": "my-name", "file": f}) 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): def test_no_file(self):
resp = self.client.post(self.url, {"name": "file.txt"}) resp = self.client.post(self.url, {"name": "file.txt"})
self.assert4XX(resp.status_code) self.assertEquals(resp.status_code, 400)
def test_get(self): def test_get(self):
resp = self.client.get(self.url) resp = self.client.get(self.url)
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
import json import json
import shutil import shutil
import mock import mock
from textwrap import dedent
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
...@@ -22,6 +25,7 @@ from contentstore.tests.utils import parse_json ...@@ -22,6 +25,7 @@ from contentstore.tests.utils import parse_json
from auth.authz import add_user_to_creator_group from auth.authz import add_user_to_creator_group
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase 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.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore import Location, mongo from xmodule.modulestore import Location, mongo
...@@ -65,7 +69,7 @@ class MongoCollectionFindWrapper(object): ...@@ -65,7 +69,7 @@ class MongoCollectionFindWrapper(object):
return self.original(query, *args, **kwargs) return self.original(query, *args, **kwargs)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
class ContentStoreToyCourseTest(ModuleStoreTestCase): class ContentStoreToyCourseTest(ModuleStoreTestCase):
""" """
Tests that rely on the toy courses. Tests that rely on the toy courses.
...@@ -312,7 +316,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -312,7 +316,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None])) handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
self.assertIn('/static/', handouts.data) 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') module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy']) import_from_xml(module_store, 'common/test/data/', ['toy'])
...@@ -845,7 +856,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -845,7 +856,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
filesystem = OSFS(root_dir / ('test_export/' + dirname)) filesystem = OSFS(root_dir / ('test_export/' + dirname))
self.assertTrue(filesystem.exists(item.location.name + filename_suffix)) 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') module_store = modulestore('direct')
draft_store = modulestore('draft') draft_store = modulestore('draft')
content_store = contentstore() content_store = contentstore()
...@@ -1122,12 +1140,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1122,12 +1140,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
wrapper = MongoCollectionFindWrapper(module_store.collection.find) wrapper = MongoCollectionFindWrapper(module_store.collection.find)
module_store.collection.find = wrapper.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) course = module_store.get_item(location, depth=2)
# make sure we haven't done too many round trips to DB # 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 # note we say 3 round trips here for 1) the course, and 2 & 3) for the chapters and sequentials
# 4) because of the RT due to calculating the inherited metadata # Because we're querying from the top of the tree, we cache information needed for inheritance,
self.assertEqual(wrapper.counter, 4) # 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 # make sure we pre-fetched a known sequential which should be at depth=2
self.assertTrue(Location(['i4x', 'edX', 'toy', 'sequential', self.assertTrue(Location(['i4x', 'edX', 'toy', 'sequential',
...@@ -1163,7 +1184,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1163,7 +1184,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
export_to_xml(module_store, content_store, location, root_dir, 'test_export') 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): class ContentStoreTest(ModuleStoreTestCase):
""" """
Tests for the CMS ContentStore application. Tests for the CMS ContentStore application.
...@@ -1408,7 +1429,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1408,7 +1429,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'Chapter 2') self.assertContains(resp, 'Chapter 2')
# go to various pages # go to various pages
...@@ -1418,92 +1439,92 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1418,92 +1439,92 @@ class ContentStoreTest(ModuleStoreTestCase):
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# export page # export page
resp = self.client.get(reverse('export_course', resp = self.client.get(reverse('export_course',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# manage users # manage users
resp = self.client.get(reverse('manage_users', resp = self.client.get(reverse('manage_users',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# course info # course info
resp = self.client.get(reverse('course_info', resp = self.client.get(reverse('course_info',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# settings_details # settings_details
resp = self.client.get(reverse('settings_details', resp = self.client.get(reverse('settings_details',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# settings_details # settings_details
resp = self.client.get(reverse('settings_grading', resp = self.client.get(reverse('settings_grading',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# static_pages # static_pages
resp = self.client.get(reverse('static_pages', resp = self.client.get(reverse('static_pages',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'coursename': loc.name})) 'coursename': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# static_pages # static_pages
resp = self.client.get(reverse('asset_index', resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# go look at a subsection page # go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence') subsection_location = loc.replace(category='sequential', name='test_sequence')
resp = self.client.get(reverse('edit_subsection', resp = self.client.get(reverse('edit_subsection',
kwargs={'location': subsection_location.url()})) kwargs={'location': subsection_location.url()}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# go look at the Edit page # go look at the Edit page
unit_location = loc.replace(category='vertical', name='test_vertical') unit_location = loc.replace(category='vertical', name='test_vertical')
resp = self.client.get(reverse('edit_unit', resp = self.client.get(reverse('edit_unit',
kwargs={'location': unit_location.url()})) kwargs={'location': unit_location.url()}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# delete a component # delete a component
del_loc = loc.replace(category='html', name='test_html') del_loc = loc.replace(category='html', name='test_html')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# delete a unit # delete a unit
del_loc = loc.replace(category='vertical', name='test_vertical') del_loc = loc.replace(category='vertical', name='test_vertical')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# delete a unit # delete a unit
del_loc = loc.replace(category='sequential', name='test_sequence') del_loc = loc.replace(category='sequential', name='test_sequence')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# delete a chapter # delete a chapter
del_loc = loc.replace(category='chapter', name='chapter_2') del_loc = loc.replace(category='chapter', name='chapter_2')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") 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): def test_import_into_new_course_id(self):
module_store = modulestore('direct') module_store = modulestore('direct')
...@@ -1690,6 +1711,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1690,6 +1711,7 @@ class ContentStoreTest(ModuleStoreTestCase):
content_store.find(location) content_store.find(location)
@override_settings(MODULESTORE=TEST_MODULESTORE)
class MetadataSaveTestCase(ModuleStoreTestCase): class MetadataSaveTestCase(ModuleStoreTestCase):
"""Test that metadata is correctly cached and decached.""" """Test that metadata is correctly cached and decached."""
......
...@@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase): ...@@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase):
def test_get(self): def test_get(self):
resp = self.client.get(self.url) resp = self.client.get(self.url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
def test_delete(self): def test_delete(self):
resp = self.client.delete(self.url) resp = self.client.delete(self.url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
def test_post(self): def test_post(self):
grader = { grader = {
...@@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase): ...@@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase):
"weight": 17.3, "weight": 17.3,
} }
resp = self.client.post(self.url, grader) resp = self.client.post(self.url, grader)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
...@@ -3,10 +3,13 @@ from unittest import skip ...@@ -3,10 +3,13 @@ from unittest import skip
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client 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.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE
@override_settings(MODULESTORE=TEST_MODULESTORE)
class InternationalizationTest(ModuleStoreTestCase): class InternationalizationTest(ModuleStoreTestCase):
""" """
Tests to validate Internationalization. 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 ...@@ -12,24 +12,26 @@ import copy
from django.contrib.auth.models import User from django.contrib.auth.models import User
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.modulestore_config import TEST_MODULESTORE
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore.xml_importer import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from uuid import uuid4 from uuid import uuid4
from pymongo import MongoClient
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex 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): class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
""" """
Tests that rely on the toy and test_import_course courses. Tests that rely on the toy and test_import_course courses.
...@@ -58,6 +60,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): ...@@ -58,6 +60,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
self.client = Client() self.client = Client()
self.client.login(username=uname, password=password) 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): def load_test_import_course(self):
''' '''
Load the standard course used to test imports (for do_import_static=False behavior). Load the standard course used to test imports (for do_import_static=False behavior).
...@@ -121,3 +127,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase): ...@@ -121,3 +127,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None])) handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
self.assertIn('/static/', handouts.data) 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): ...@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
resp.content, resp.content,
"application/json" "application/json"
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
class TestCreateItem(CourseTestCase): class TestCreateItem(CourseTestCase):
......
...@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase):
def test_view_index(self): def test_view_index(self):
"Basic check that the textbook index page responds correctly" "Basic check that the textbook index page responds correctly"
resp = self.client.get(self.url) 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, # we don't have resp.context right now,
# due to bugs in our testing harness :( # due to bugs in our testing harness :(
if resp.context: if resp.context:
...@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertEqual(self.course.pdf_textbooks, obj) self.assertEqual(self.course.pdf_textbooks, obj)
...@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertEqual(content, obj) self.assertEqual(content, obj)
...@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# reload course # reload course
store = get_modulestore(self.course.location) store = get_modulestore(self.course.location)
...@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertIn("error", obj) self.assertIn("error", obj)
...@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest", HTTP_X_REQUESTED_WITH="XMLHttpRequest",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
self.assertNotIn("Location", resp) self.assertNotIn("Location", resp)
...@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase):
def test_get_1(self): def test_get_1(self):
"Get the first textbook" "Get the first textbook"
resp = self.client.get(self.url1) resp = self.client.get(self.url1)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content) compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook1) self.assertEqual(compare, self.textbook1)
def test_get_2(self): def test_get_2(self):
"Get the second textbook" "Get the second textbook"
resp = self.client.get(self.url2) resp = self.client.get(self.url2)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content) compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook2) self.assertEqual(compare, self.textbook2)
...@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase):
def test_delete(self): def test_delete(self):
"Delete a textbook by ID" "Delete a textbook by ID"
resp = self.client.delete(self.url1) 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) course = self.store.get_item(self.course.location)
self.assertEqual(course.pdf_textbooks, [self.textbook2]) self.assertEqual(course.pdf_textbooks, [self.textbook2])
...@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase):
) )
self.assertEqual(resp.status_code, 201) self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(url) resp2 = self.client.get(url)
self.assert2XX(resp2.status_code) self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content) compare = json.loads(resp2.content)
self.assertEqual(compare, textbook) self.assertEqual(compare, textbook)
course = self.store.get_item(self.course.location) course = self.store.get_item(self.course.location)
...@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase):
) )
self.assertEqual(resp.status_code, 201) self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(self.url2) resp2 = self.client.get(self.url2)
self.assert2XX(resp2.status_code) self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content) compare = json.loads(resp2.content)
self.assertEqual(compare, replacement) self.assertEqual(compare, replacement)
course = self.store.get_item(self.course.location) course = self.store.get_item(self.course.location)
......
...@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase): ...@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase):
def test_detail_inactive(self): def test_detail_inactive(self):
resp = self.client.get(self.inactive_detail_url) resp = self.client.get(self.inactive_detail_url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertFalse(result["active"]) self.assertFalse(result["active"])
def test_detail_invalid(self): def test_detail_invalid(self):
resp = self.client.get(self.invalid_detail_url) resp = self.client.get(self.invalid_detail_url)
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 404)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
...@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase): ...@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
data={"role": None}, data={"role": None},
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase): ...@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase): ...@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase): ...@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase): ...@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
self.assert_not_enrolled() self.assert_not_enrolled()
...@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase): ...@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
self.assert_not_enrolled() self.assert_not_enrolled()
...@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase): ...@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "staff"}, data={"role": "staff"},
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase): ...@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase): ...@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase): ...@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"}, data={"role": "instructor"},
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
...@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase): ...@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"}, data={"role": "instructor"},
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
...@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase): ...@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase):
}) })
resp = self.client.delete(self_url) resp = self.client.delete(self_url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
user = User.objects.get(email=self.user.email) user = User.objects.get(email=self.user.email)
groups = [g.name for g in user.groups.all()] groups = [g.name for g in user.groups.all()]
...@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase): ...@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase):
self.ext_user.save() self.ext_user.save()
resp = self.client.delete(self.detail_url) resp = self.client.delete(self.detail_url)
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
# reload user from DB # reload user from DB
...@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase): ...@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
self.assert_enrolled() self.assert_enrolled()
def test_staff_to_instructor_still_enrolled(self): def test_staff_to_instructor_still_enrolled(self):
...@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase): ...@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
self.assert_enrolled() self.assert_enrolled()
def assert_not_enrolled(self): def assert_not_enrolled(self):
......
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings
from django.core.cache import cache from django.core.cache import cache
from django.core.urlresolvers import reverse 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 xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from contentstore.tests.test_course_settings import CourseTestCase from contentstore.tests.test_course_settings import CourseTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from contentstore.tests.modulestore_config import TEST_MODULESTORE
import datetime import datetime
from pytz import UTC from pytz import UTC
@override_settings(MODULESTORE=TEST_MODULESTORE)
class ContentStoreTestCase(ModuleStoreTestCase): class ContentStoreTestCase(ModuleStoreTestCase):
def _login(self, email, password): def _login(self, email, password):
""" """
......
...@@ -7,9 +7,11 @@ import json ...@@ -7,9 +7,11 @@ import json
from student.models import Registration from student.models import Registration
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client 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.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from contentstore.tests.modulestore_config import TEST_MODULESTORE
def parse_json(response): def parse_json(response):
...@@ -27,6 +29,7 @@ def registration(email): ...@@ -27,6 +29,7 @@ def registration(email):
return Registration.objects.get(user__email=email) return Registration.objects.get(user__email=email)
@override_settings(MODULESTORE=TEST_MODULESTORE)
class CourseTestCase(ModuleStoreTestCase): class CourseTestCase(ModuleStoreTestCase):
def setUp(self): def setUp(self):
""" """
......
...@@ -10,6 +10,7 @@ from .component import * ...@@ -10,6 +10,7 @@ from .component import *
from .course import * from .course import *
from .error import * from .error import *
from .item import * from .item import *
from .import_export import *
from .preview import * from .preview import *
from .public import * from .public import *
from .user import * from .user import *
......
...@@ -4,6 +4,7 @@ import os ...@@ -4,6 +4,7 @@ import os
import tarfile import tarfile
import shutil import shutil
import cgi import cgi
import re
from functools import partial from functools import partial
from tempfile import mkdtemp from tempfile import mkdtemp
from path import path from path import path
...@@ -35,9 +36,7 @@ from .access import get_location_and_verify_access ...@@ -35,9 +36,7 @@ from .access import get_location_and_verify_access
from util.json_request import JsonResponse from util.json_request import JsonResponse
__all__ = ['asset_index', 'upload_asset', 'import_course', __all__ = ['asset_index', 'upload_asset']
'generate_export_course', 'export_course']
def assets_to_json_dict(assets): def assets_to_json_dict(assets):
""" """
...@@ -167,7 +166,7 @@ def upload_asset(request, org, course, coursename): ...@@ -167,7 +166,7 @@ def upload_asset(request, org, course, coursename):
sc_partial = partial(StaticContent, content_loc, filename, mime_type) sc_partial = partial(StaticContent, content_loc, filename, mime_type)
if chunked: if chunked:
content = sc_partial(upload_file.chunks()) content = sc_partial(upload_file.chunks())
temp_filepath = upload_file.temporary_file_path() tempfile_path = upload_file.temporary_file_path()
else: else:
content = sc_partial(upload_file.read()) content = sc_partial(upload_file.read())
tempfile_path = None tempfile_path = None
...@@ -260,179 +259,3 @@ def remove_asset(request, org, course, name): ...@@ -260,179 +259,3 @@ def remove_asset(request, org, course, name):
return HttpResponse() 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 ...@@ -18,6 +18,7 @@ from mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.exceptions import ( from xmodule.modulestore.exceptions import (
ItemNotFoundError, InvalidLocationError) ItemNotFoundError, InvalidLocationError)
...@@ -206,7 +207,8 @@ def course_info(request, org, course, name, provided_id=None): ...@@ -206,7 +207,8 @@ def course_info(request, org, course, name, provided_id=None):
'context_course': course_module, 'context_course': course_module,
'url_base': "/" + org + "/" + course + "/", 'url_base': "/" + org + "/" + course + "/",
'course_updates': json.dumps(get_course_updates(location)), '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 @expect_json
......
...@@ -246,7 +246,7 @@ PIPELINE_JS = { ...@@ -246,7 +246,7 @@ PIPELINE_JS = {
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js', 'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
'js/models/uploads.js', 'js/views/uploads.js', 'js/models/uploads.js', 'js/views/uploads.js',
'js/models/textbook.js', 'js/views/textbook.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'], 'js/models/settings/course_grading_policy.js'],
'output_filename': 'js/cms-application.js', 'output_filename': 'js/cms-application.js',
'test_order': 0 '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 # Stub jQuery.cookie
@stubCookies = @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 ...@@ -64,20 +64,31 @@ class CMS.Views.TabsEdit extends Backbone.View
course: course_location_analytics course: course_location_analytics
deleteTab: (event) => deleteTab: (event) =>
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.' confirm = new CMS.Views.Prompt.Warning
return title: gettext('Delete Component Confirmation')
$component = $(event.currentTarget).parents('.component') message: gettext('Are you sure you want to delete this component? This action cannot be undone.')
actions:
analytics.track "Deleted Static Page", primary:
course: course_location_analytics text: gettext("OK")
id: $component.data('id') click: (view) ->
view.hide()
$.post('/delete_item', { $component = $(event.currentTarget).parents('.component')
id: $component.data('id')
}, => analytics.track "Deleted Static Page",
$component.remove() 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 @@ ...@@ -3,6 +3,26 @@
The render here adds views for each update/handout by delegating to their collections but does not The render here adds views for each update/handout by delegating to their collections but does not
generate any html for the surrounding page. 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({ CMS.Views.CourseInfoEdit = Backbone.View.extend({
// takes CMS.Models.CourseInfo as model // takes CMS.Models.CourseInfo as model
tagName: 'div', tagName: 'div',
...@@ -11,18 +31,19 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({ ...@@ -11,18 +31,19 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
// instantiate the ClassInfoUpdateView and delegate the proper dom to it // instantiate the ClassInfoUpdateView and delegate the proper dom to it
new CMS.Views.ClassInfoUpdateView({ new CMS.Views.ClassInfoUpdateView({
el: $('body.updates'), 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({ new CMS.Views.ClassInfoHandoutsView({
el: this.$('#course-handouts-view'), 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; return this;
} }
}); });
// ??? Programming style question: should each of these classes be in separate files?
CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
// collection is CourseUpdateCollection // collection is CourseUpdateCollection
events: { events: {
...@@ -48,6 +69,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -48,6 +69,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
var self = this; var self = this;
this.collection.each(function (update) { this.collection.each(function (update) {
try { try {
changeContentToPreview(update, 'content', self.options['base_asset_url'])
var newEle = self.template({ updateModel : update }); var newEle = self.template({ updateModel : update });
$(updateEle).append(newEle); $(updateEle).append(newEle);
} catch (e) { } catch (e) {
...@@ -72,20 +94,18 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -72,20 +94,18 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$(updateEle).prepend($newForm); $(updateEle).prepend($newForm);
var $textArea = $newForm.find(".new-update-content").first(); var $textArea = $newForm.find(".new-update-content").first();
if (this.$codeMirror == null ) { this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), { mode: "text/html",
mode: "text/html", lineNumbers: true,
lineNumbers: true, lineWrapping: true
lineWrapping: true, });
});
}
$newForm.addClass('editing'); $newForm.addClass('editing');
this.$currentPost = $newForm.closest('li'); this.$currentPost = $newForm.closest('li');
window.$modalCover.show(); window.$modalCover.show();
window.$modalCover.bind('click', function() { window.$modalCover.bind('click', function() {
self.closeEditor(self, true); self.closeEditor(true);
}); });
$('.date').datepicker('destroy'); $('.date').datepicker('destroy');
...@@ -110,7 +130,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -110,7 +130,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
ele.remove(); ele.remove();
} }
}); });
this.closeEditor(this); this.closeEditor();
analytics.track('Saved Course Update', { analytics.track('Saved Course Update', {
'course': course_location_analytics, 'course': course_location_analytics,
...@@ -122,8 +142,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -122,8 +142,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
event.preventDefault(); event.preventDefault();
// change editor contents back to model values and hide the editor // change editor contents back to model values and hide the editor
$(this.editor(event)).hide(); $(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); var targetModel = this.eventModel(event);
this.closeEditor(this, !targetModel.id); this.closeEditor(!targetModel.id);
}, },
onEdit: function(event) { onEdit: function(event) {
...@@ -134,16 +156,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -134,16 +156,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
$(this.editor(event)).show(); $(this.editor(event)).show();
var $textArea = this.$currentPost.find(".new-update-content").first(); var $textArea = this.$currentPost.find(".new-update-content").first();
if (this.$codeMirror == null ) { var targetModel = this.eventModel(event);
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), { this.$codeMirror = editWithCodeMirror(targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
window.$modalCover.show(); window.$modalCover.show();
var targetModel = this.eventModel(event);
window.$modalCover.bind('click', function() { window.$modalCover.bind('click', function() {
self.closeEditor(self); self.closeEditor(self);
}); });
...@@ -193,31 +209,35 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({ ...@@ -193,31 +209,35 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
} }
}); });
confirm.show(); confirm.show();
}, },
closeEditor: function(self, removePost) { closeEditor: function(removePost) {
var targetModel = self.collection.get(self.$currentPost.attr('name')); var targetModel = this.collection.get(this.$currentPost.attr('name'));
if(removePost) { if(removePost) {
self.$currentPost.remove(); this.$currentPost.remove();
} }
else {
// close the modal and insert the appropriate data // close the modal and insert the appropriate data
self.$currentPost.removeClass('editing'); this.$currentPost.removeClass('editing');
self.$currentPost.find('.date-display').html(targetModel.get('date')); this.$currentPost.find('.date-display').html(targetModel.get('date'));
self.$currentPost.find('.date').val(targetModel.get('date')); this.$currentPost.find('.date').val(targetModel.get('date'));
try {
// just in case the content causes an error (embedded js errors) var content = changeContentToPreview(targetModel, 'content', this.options['base_asset_url'])
self.$currentPost.find('.update-contents').html(targetModel.get('content')); try {
self.$currentPost.find('.new-update-content').val(targetModel.get('content')); // just in case the content causes an error (embedded js errors)
} catch (e) { this.$currentPost.find('.update-contents').html(content);
// ignore but handle rest of page 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.unbind('click');
window.$modalCover.hide(); window.$modalCover.hide();
this.$codeMirror = null; this.$codeMirror = null;
self.$currentPost.find('.CodeMirror').remove();
}, },
// Dereferencing from events to screen elements // Dereferencing from events to screen elements
...@@ -275,8 +295,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -275,8 +295,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
}, },
render: function () { render: function () {
var updateEle = this.$el; changeContentToPreview(this.model, 'data', this.options['base_asset_url'])
var self = this;
this.$el.html( this.$el.html(
$(this.template( { $(this.template( {
model: this.model model: this.model
...@@ -295,22 +315,17 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -295,22 +315,17 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
var self = this; var self = this;
this.$editor.val(this.$preview.html()); this.$editor.val(this.$preview.html());
this.$form.show(); this.$form.show();
if (this.$codeMirror == null) {
this.$codeMirror = CodeMirror.fromTextArea(this.$editor.get(0), { this.$codeMirror = editWithCodeMirror(self.model, 'data', self.options['base_asset_url'], this.$editor.get(0));
mode: "text/html",
lineNumbers: true,
lineWrapping: true,
});
}
window.$modalCover.show(); window.$modalCover.show();
window.$modalCover.bind('click', function() { window.$modalCover.bind('click', function() {
self.closeEditor(self); self.closeEditor();
}); });
}, },
onSave: function(event) { onSave: function(event) {
this.model.set('data', this.$codeMirror.getValue()); this.model.set('data', this.$codeMirror.getValue());
this.render();
var saving = new CMS.Views.Notification.Mini({ var saving = new CMS.Views.Notification.Mini({
title: gettext('Saving') + '&hellip;' title: gettext('Saving') + '&hellip;'
}); });
...@@ -320,8 +335,9 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -320,8 +335,9 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
saving.hide(); saving.hide();
} }
}); });
this.render();
this.$form.hide(); this.$form.hide();
this.closeEditor(this); this.closeEditor();
analytics.track('Saved Course Handouts', { analytics.track('Saved Course Handouts', {
'course': course_location_analytics 'course': course_location_analytics
...@@ -331,14 +347,14 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({ ...@@ -331,14 +347,14 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
onCancel: function(event) { onCancel: function(event) {
this.$form.hide(); this.$form.hide();
this.closeEditor(this); this.closeEditor();
}, },
closeEditor: function(self) { closeEditor: function() {
this.$form.hide(); this.$form.hide();
window.$modalCover.unbind('click'); window.$modalCover.unbind('click');
window.$modalCover.hide(); window.$modalCover.hide();
self.$form.find('.CodeMirror').remove(); this.$form.find('.CodeMirror').remove();
this.$codeMirror = null; 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 { ...@@ -57,6 +57,11 @@ body.course.import {
color: $error-red; color: $error-red;
} }
.status-block {
display: none;
font-size: 13px;
}
.choose-file-button { .choose-file-button {
@include blue-button; @include blue-button;
padding: 10px 50px 11px; padding: 10px 50px 11px;
......
../../common/lib/xmodule/xmodule/js/
\ No newline at end of file
...@@ -39,6 +39,7 @@ ...@@ -39,6 +39,7 @@
model : new CMS.Models.CourseInfo({ model : new CMS.Models.CourseInfo({
courseId : '${context_course.location}', courseId : '${context_course.location}',
updates : course_updates, updates : course_updates,
base_asset_url : '${base_asset_url}',
handouts : course_handouts handouts : course_handouts
}) })
}); });
......
...@@ -25,13 +25,16 @@ ...@@ -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>${_("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> <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> </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> <h2>${_("Course to import:")}</h2>
<p class="error-block"></p> <p class="error-block"></p>
<a href="#" class="choose-file-button">${_("Choose File")}</a> <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> <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="file" name="course-data" class="file-input" >
<input type="submit" value="${_('Replace my course with the one above')}" class="submit-button"> <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-bar">
<div class="progress-fill"></div> <div class="progress-fill"></div>
<div class="percent">0%</div> <div class="percent">0%</div>
...@@ -43,6 +46,9 @@ ...@@ -43,6 +46,9 @@
</%block> </%block>
<%block name="jsextra"> <%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> <script>
(function() { (function() {
...@@ -50,33 +56,61 @@ var bar = $('.progress-bar'); ...@@ -50,33 +56,61 @@ var bar = $('.progress-bar');
var fill = $('.progress-fill'); var fill = $('.progress-fill');
var percent = $('.percent'); var percent = $('.percent');
var status = $('#status'); var status = $('#status');
var statusBlock = $('.status-block');
var submitBtn = $('.submit-button'); var submitBtn = $('.submit-button');
$('form').ajaxForm({
beforeSend: function() { $('#fileupload').fileupload({
status.empty();
var percentVal = '0%'; dataType: 'json',
bar.show(); type: 'POST',
fill.width(percentVal);
percent.html(percentVal); maxChunkSize: 20 * 1000000, // 20 MB
submitBtn.hide();
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); fill.width(percentVal);
percent.html(percentVal); percent.html(percentVal);
}, },
complete: function(xhr) { done: function(e, data){
if (xhr.status == 200) { bar.hide();
alert('${_("Your import was successful.")}'); alert('${_("Your import was successful.")}');
window.location = '${successful_import_redirect_url}'; window.location = '${successful_import_redirect_url}';
} },
else sequentialUploads: true
alert('${_("Your import has failed.")}\n\n' + xhr.responseText);
submitBtn.show();
bar.hide();
} });
});
})(); })();
</script> </script>
</%block> </%block>
from django.conf import settings from django.conf import settings
from django.conf.urls import patterns, include, url 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. # TODO: This should be removed once the CMS is running via wsgi on all production servers
# pylint: disable=W0611 import cms.startup as startup
from . import one_time_startup startup.run()
# There is a course creators admin table. # There is a course creators admin table.
from ratelimitbackend import admin from ratelimitbackend import admin
...@@ -135,10 +135,6 @@ urlpatterns += ( ...@@ -135,10 +135,6 @@ urlpatterns += (
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), 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'): if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
urlpatterns += ( urlpatterns += (
url(r'^status/', include('service_status.urls')), 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 ...@@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts, from course_groups.cohorts import (get_cohort, get_course_cohorts,
is_commentable_cohorted, get_cohort_by_name) 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 # NOTE: running this with the lms.envs.test config works without
# manually overriding the modulestore. However, running with # manually overriding the modulestore. However, running with
# cms.envs.test doesn't. # cms.envs.test doesn't.
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT 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): class TestCohorts(django.test.TestCase):
@staticmethod @staticmethod
...@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase): ...@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase):
""" """
Make sure that course is reloaded every time--clear out the modulestore. 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 clear_existing_modulestores()
# to course. We don't have a course.clone() method.
_MODULESTORES.clear()
def test_get_cohort(self): def test_get_cohort(self):
""" """
......
...@@ -57,11 +57,6 @@ class CourseMode(models.Model): ...@@ -57,11 +57,6 @@ class CourseMode(models.Model):
def modes_for_course_dict(cls, course_id): def modes_for_course_dict(cls, course_id):
return { mode.slug : mode for mode in cls.modes_for_course(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 @classmethod
def mode_for_course(cls, course_id, mode_slug): def mode_for_course(cls, course_id, mode_slug):
""" """
...@@ -76,3 +71,8 @@ class CourseMode(models.Model): ...@@ -76,3 +71,8 @@ class CourseMode(models.Model):
return matched[0] return matched[0]
else: else:
return None 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 ...@@ -14,11 +14,9 @@ from django.contrib.auth.models import AnonymousUser, User
from django.utils.importlib import import_module from django.utils.importlib import import_module
from xmodule.modulestore.tests.factories import CourseFactory 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.inheritance import own_metadata
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import editable_modulestore
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from external_auth.views import shib_login, course_specific_login, course_specific_register 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 ...@@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment
from student.models import UserProfile, Registration, CourseEnrollment from student.models import UserProfile, Registration, CourseEnrollment
from student.tests.factories import UserFactory 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' # 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 # 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 # 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(): ...@@ -64,7 +64,7 @@ def gen_all_identities():
yield _build_identity_dict(mail, given_name, surname) 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): class ShibSPTest(ModuleStoreTestCase):
""" """
Tests for the Shibboleth SP, which communicates via request.META Tests for the Shibboleth SP, which communicates via request.META
...@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase): ...@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase):
request_factory = RequestFactory() request_factory = RequestFactory()
def setUp(self): def setUp(self):
self.store = modulestore() self.store = editable_modulestore()
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
def test_exception_shib_login(self): def test_exception_shib_login(self):
......
...@@ -12,36 +12,11 @@ ...@@ -12,36 +12,11 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from mako.lookup import TemplateLookup
import tempdir
from django.template import RequestContext from django.template import RequestContext
from django.conf import settings
requestcontext = None requestcontext = None
lookup = {}
class MakoMiddleware(object): 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): def process_request(self, request):
global requestcontext global requestcontext
......
...@@ -16,7 +16,7 @@ from django.template import Context ...@@ -16,7 +16,7 @@ from django.template import Context
from django.http import HttpResponse from django.http import HttpResponse
import logging import logging
from . import middleware import mitxmako
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -80,15 +80,15 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'): ...@@ -80,15 +80,15 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_instance['marketing_link'] = marketing_link context_instance['marketing_link'] = marketing_link
# In various testing contexts, there might not be a current request context. # In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None: if mitxmako.middleware.requestcontext is not None:
for d in middleware.requestcontext: for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d) context_dictionary.update(d)
for d in context_instance: for d in context_instance:
context_dictionary.update(d) context_dictionary.update(d)
if context: if context:
context_dictionary.update(context) context_dictionary.update(context)
# fetch and render template # 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) 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 ...@@ -16,7 +16,8 @@ from django.conf import settings
from mako.template import Template as MakoTemplate from mako.template import Template as MakoTemplate
from mitxmako.shortcuts import marketing_link from mitxmako.shortcuts import marketing_link
from mitxmako import middleware import mitxmako
import mitxmako.middleware
django_variables = ['lookup', 'output_encoding', 'encoding_errors'] django_variables = ['lookup', 'output_encoding', 'encoding_errors']
...@@ -33,7 +34,7 @@ class Template(MakoTemplate): ...@@ -33,7 +34,7 @@ class Template(MakoTemplate):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides""" """Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False): 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'] overrides['lookup'] = overrides['lookup']['main']
kwargs.update(overrides) kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs) super(Template, self).__init__(*args, **kwargs)
...@@ -47,8 +48,8 @@ class Template(MakoTemplate): ...@@ -47,8 +48,8 @@ class Template(MakoTemplate):
context_dictionary = {} context_dictionary = {}
# In various testing contexts, there might not be a current request context. # In various testing contexts, there might not be a current request context.
if middleware.requestcontext is not None: if mitxmako.middleware.requestcontext is not None:
for d in middleware.requestcontext: for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d) context_dictionary.update(d)
for d in context_instance: for d in context_instance:
context_dictionary.update(d) context_dictionary.update(d)
......
import re 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, from static_replace import (replace_static_urls, replace_course_urls,
_url_replace_regex) _url_replace_regex)
from mock import patch, Mock from mock import patch, Mock
......
...@@ -16,10 +16,6 @@ from django.contrib.auth.models import User ...@@ -16,10 +16,6 @@ from django.contrib.auth.models import User
from student.models import UserProfile from student.models import UserProfile
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
......
...@@ -12,10 +12,6 @@ from django.contrib.auth.models import User ...@@ -12,10 +12,6 @@ from django.contrib.auth.models import User
from student.models import UserProfile from student.models import UserProfile
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
def import_user(u): def import_user(u):
user_info = u['u'] user_info = u['u']
......
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth.models import User from django.contrib.auth.models import User
import mitxmako.middleware as middleware
from student.models import UserTestGroup from student.models import UserTestGroup
import random import random
...@@ -11,8 +10,6 @@ import datetime ...@@ -11,8 +10,6 @@ import datetime
import json import json
from pytz import UTC from pytz import UTC
middleware.MakoMiddleware()
def group_from_value(groups, v): def group_from_value(groups, v):
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value ''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
......
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth.models import User from django.contrib.auth.models import User
import mitxmako.middleware as middleware
middleware.MakoMiddleware()
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
......
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth.models import User from django.contrib.auth.models import User
import mitxmako.middleware as middleware import mitxmako
middleware.MakoMiddleware()
class Command(BaseCommand): class Command(BaseCommand):
...@@ -17,8 +15,8 @@ body, and an _subject.txt for the subject. ''' ...@@ -17,8 +15,8 @@ body, and an _subject.txt for the subject. '''
#text = open(args[0]).read() #text = open(args[0]).read()
#subject = open(args[1]).read() #subject = open(args[1]).read()
users = User.objects.all() users = User.objects.all()
text = middleware.lookup['main'].get_template('email/' + args[0] + ".txt").render() text = mitxmako.lookup['main'].get_template('email/' + args[0] + ".txt").render()
subject = middleware.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip() subject = mitxmako.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
for user in users: for user in users:
if user.is_active: if user.is_active:
user.email_user(subject, text) user.email_user(subject, text)
...@@ -4,15 +4,13 @@ import time ...@@ -4,15 +4,13 @@ import time
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
import mitxmako.middleware as middleware import mitxmako
from django.core.mail import send_mass_mail from django.core.mail import send_mass_mail
import sys import sys
import datetime import datetime
middleware.MakoMiddleware()
def chunks(l, n): def chunks(l, n):
""" Yield successive n-sized chunks from l. """ Yield successive n-sized chunks from l.
...@@ -41,8 +39,8 @@ rate -- messages per second ...@@ -41,8 +39,8 @@ rate -- messages per second
users = [u.strip() for u in open(user_file).readlines()] users = [u.strip() for u in open(user_file).readlines()]
message = middleware.lookup['main'].get_template('emails/' + message_base + "_body.txt").render() message = mitxmako.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
subject = middleware.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip() subject = mitxmako.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
rate = int(ratestr) rate = int(ratestr)
self.log_file = open(logfilename, "a+", buffering=0) self.log_file = open(logfilename, "a+", buffering=0)
......
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth.models import User from django.contrib.auth.models import User
import mitxmako.middleware as middleware
import json import json
from student.models import UserProfile from student.models import UserProfile
middleware.MakoMiddleware()
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
......
...@@ -805,7 +805,8 @@ class CourseEnrollment(models.Model): ...@@ -805,7 +805,8 @@ class CourseEnrollment(models.Model):
record.is_active = False record.is_active = False
record.save() record.save()
except cls.DoesNotExist: 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 @classmethod
def unenroll_by_email(cls, email, course_id): def unenroll_by_email(cls, email, course_id):
......
"""
Student Views
"""
import datetime import datetime
import json import json
import logging import logging
...@@ -52,6 +55,10 @@ from courseware.access import has_access ...@@ -52,6 +55,10 @@ from courseware.access import has_access
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from bulk_email.models import Optout
import track.views
from statsd import statsd from statsd import statsd
from pytz import UTC from pytz import UTC
...@@ -62,8 +69,7 @@ Article = namedtuple('Article', 'title url author image deck publication publish ...@@ -62,8 +69,7 @@ Article = namedtuple('Article', 'title url author image deck publication publish
def csrf_token(context): 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', '') csrf_token = context.get('csrf_token', '')
if csrf_token == 'NOTPROVIDED': if csrf_token == 'NOTPROVIDED':
return '' return ''
...@@ -76,12 +82,12 @@ def csrf_token(context): ...@@ -76,12 +82,12 @@ def csrf_token(context):
# This means that it should always return the same thing for anon # This means that it should always return the same thing for anon
# users. (in particular, no switching based on query params allowed) # users. (in particular, no switching based on query params allowed)
def index(request, extra_context={}, user=None): def index(request, extra_context={}, user=None):
''' """
Render the edX main page. Render the edX main page.
extra_context is used to allow immediate display of certain modal windows, eg signup, extra_context is used to allow immediate display of certain modal windows, eg signup,
as used by external_auth. as used by external_auth.
''' """
# The course selection work is done in courseware.courses. # The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
...@@ -265,6 +271,8 @@ def dashboard(request): ...@@ -265,6 +271,8 @@ def dashboard(request):
log.error("User {0} enrolled in non-existent course {1}" log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id)) .format(user.username, enrollment.course_id))
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
message = "" message = ""
if not user.is_active: if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email}) message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
...@@ -292,6 +300,7 @@ def dashboard(request): ...@@ -292,6 +300,7 @@ def dashboard(request):
pass pass
context = {'courses': courses, context = {'courses': courses,
'course_optouts': course_optouts,
'message': message, 'message': message,
'external_auth_map': external_auth_map, 'external_auth_map': external_auth_map,
'staff_access': staff_access, 'staff_access': staff_access,
...@@ -411,7 +420,7 @@ def accounts_login(request, error=""): ...@@ -411,7 +420,7 @@ def accounts_login(request, error=""):
# Need different levels of logging # Need different levels of logging
@ensure_csrf_cookie @ensure_csrf_cookie
def login_user(request, error=""): 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: if 'email' not in request.POST or 'password' not in request.POST:
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'value': _('There was an error receiving your login information. Please email us.')})) # TODO: User error message '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=""): ...@@ -494,11 +503,11 @@ def login_user(request, error=""):
@ensure_csrf_cookie @ensure_csrf_cookie
def logout_user(request): def logout_user(request):
''' """
HTTP request to log out the user. Redirects to marketing page. HTTP request to log out the user. Redirects to marketing page.
Deletes both the CSRF and sessionid cookies so the marketing Deletes both the CSRF and sessionid cookies so the marketing
site can determine the logged in state of the user site can determine the logged in state of the user
''' """
# We do not log here, because we have a handler registered # We do not log here, because we have a handler registered
# to perform logging on successful logouts. # to perform logging on successful logouts.
logout(request) logout(request)
...@@ -512,8 +521,7 @@ def logout_user(request): ...@@ -512,8 +521,7 @@ def logout_user(request):
@login_required @login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def change_setting(request): 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 # TODO (vshnayder): location is no longer used
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
if 'location' in request.POST: if 'location' in request.POST:
...@@ -581,10 +589,10 @@ def _do_create_account(post_vars): ...@@ -581,10 +589,10 @@ def _do_create_account(post_vars):
@ensure_csrf_cookie @ensure_csrf_cookie
def create_account(request, post_override=None): def create_account(request, post_override=None):
''' """
JSON call to create new edX account. JSON call to create new edX account.
Used by form in signup_modal.html, which is included into navigation.html Used by form in signup_modal.html, which is included into navigation.html
''' """
js = {'success': False} js = {'success': False}
post_vars = post_override if post_override else request.POST post_vars = post_override if post_override else request.POST
...@@ -818,10 +826,10 @@ def begin_exam_registration(request, course_id): ...@@ -818,10 +826,10 @@ def begin_exam_registration(request, course_id):
@ensure_csrf_cookie @ensure_csrf_cookie
def create_exam_registration(request, post_override=None): def create_exam_registration(request, post_override=None):
''' """
JSON call to create a test center exam registration. JSON call to create a test center exam registration.
Called by form in test_center_register.html Called by form in test_center_register.html
''' """
post_vars = post_override if post_override else request.POST 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 # 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): ...@@ -974,8 +982,7 @@ def auto_auth(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def activate_account(request, key): 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) r = Registration.objects.filter(activation_key=key)
if len(r) == 1: if len(r) == 1:
user_logged_in = request.user.is_authenticated() user_logged_in = request.user.is_authenticated()
...@@ -1010,7 +1017,7 @@ def activate_account(request, key): ...@@ -1010,7 +1017,7 @@ def activate_account(request, key):
@ensure_csrf_cookie @ensure_csrf_cookie
def password_reset(request): def password_reset(request):
''' Attempts to send a password reset e-mail. ''' """ Attempts to send a password reset e-mail. """
if request.method != "POST": if request.method != "POST":
raise Http404 raise Http404
...@@ -1032,9 +1039,9 @@ def password_reset_confirm_wrapper( ...@@ -1032,9 +1039,9 @@ def password_reset_confirm_wrapper(
uidb36=None, uidb36=None,
token=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. Needed because we want to set the user as active at this step.
''' """
# cribbed from django.contrib.auth.views.password_reset_confirm # cribbed from django.contrib.auth.views.password_reset_confirm
try: try:
uid_int = base36_to_int(uidb36) uid_int = base36_to_int(uidb36)
...@@ -1076,8 +1083,8 @@ def reactivation_email_for_user(user): ...@@ -1076,8 +1083,8 @@ def reactivation_email_for_user(user):
@ensure_csrf_cookie @ensure_csrf_cookie
def change_email_request(request): 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 ## Make sure it checks for existing e-mail conflicts
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise Http404 raise Http404
...@@ -1132,9 +1139,9 @@ def change_email_request(request): ...@@ -1132,9 +1139,9 @@ def change_email_request(request):
@ensure_csrf_cookie @ensure_csrf_cookie
@transaction.commit_manually @transaction.commit_manually
def confirm_email_change(request, key): 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 link is clicked. We confirm with the old e-mail, and update
''' """
try: try:
try: try:
pec = PendingEmailChange.objects.get(activation_key=key) pec = PendingEmailChange.objects.get(activation_key=key)
...@@ -1191,7 +1198,7 @@ def confirm_email_change(request, key): ...@@ -1191,7 +1198,7 @@ def confirm_email_change(request, key):
@ensure_csrf_cookie @ensure_csrf_cookie
def change_name_request(request): 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: if not request.user.is_authenticated:
raise Http404 raise Http404
...@@ -1215,7 +1222,7 @@ def change_name_request(request): ...@@ -1215,7 +1222,7 @@ def change_name_request(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def pending_name_changes(request): 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: if not request.user.is_staff:
raise Http404 raise Http404
...@@ -1231,7 +1238,7 @@ def pending_name_changes(request): ...@@ -1231,7 +1238,7 @@ def pending_name_changes(request):
@ensure_csrf_cookie @ensure_csrf_cookie
def reject_name_change(request): 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: if not request.user.is_staff:
raise Http404 raise Http404
...@@ -1269,13 +1276,36 @@ def accept_name_change_by_id(id): ...@@ -1269,13 +1276,36 @@ def accept_name_change_by_id(id):
@ensure_csrf_cookie @ensure_csrf_cookie
def accept_name_change(request): 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 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 of manually approving them. Still keeping this around in case we want to go
back to this approval method. back to this approval method.
''' """
if not request.user.is_staff: if not request.user.is_staff:
raise Http404 raise Http404
return accept_name_change_by_id(int(request.POST['id'])) 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 ...@@ -16,11 +16,6 @@ from requests import put
from base64 import encodestring from base64 import encodestring
from json import dumps 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 from pymongo import MongoClient
import xmodule.modulestore.django import xmodule.modulestore.django
from xmodule.contentstore.django import _CONTENTSTORE from xmodule.contentstore.django import _CONTENTSTORE
...@@ -161,9 +156,10 @@ def reset_databases(scenario): ...@@ -161,9 +156,10 @@ def reset_databases(scenario):
mongo = MongoClient() mongo = MongoClient()
mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db']) mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear() _CONTENTSTORE.clear()
modulestore = xmodule.modulestore.django.modulestore()
modulestore = xmodule.modulestore.django.editable_modulestore()
modulestore.collection.drop() modulestore.collection.drop()
xmodule.modulestore.django._MODULESTORES.clear() xmodule.modulestore.django.clear_existing_modulestores()
# Uncomment below to trigger a screenshot on error # Uncomment below to trigger a screenshot on error
......
...@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login ...@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login
from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment 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 xmodule.contentstore.django import contentstore
from urllib import quote_plus from urllib import quote_plus
...@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False): ...@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False):
@world.absorb @world.absorb
def clear_courses(): def clear_courses():
# Flush and initialize the module store # 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 # Note that if your test module gets in some weird state
# (though it shouldn't), do this manually # (though it shouldn't), do this manually
# from the bash shell to drop it: # from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()" # $ mongo test_xmodule --eval "db.dropDatabase()"
modulestore().collection.drop() editable_modulestore().collection.drop()
contentstore().fs_files.drop() contentstore().fs_files.drop()
...@@ -15,7 +15,7 @@ from lettuce import world, step ...@@ -15,7 +15,7 @@ from lettuce import world, step
from .course_helpers import * from .course_helpers import *
from .ui_helpers import * from .ui_helpers import *
from lettuce.django import django_url 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 from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
......
...@@ -10,7 +10,7 @@ from selenium.webdriver.support import expected_conditions as EC ...@@ -10,7 +10,7 @@ from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from lettuce.django import django_url from lettuce.django import django_url
from nose.tools import assert_true from nose.tools import assert_true # pylint: disable=E0611
@world.absorb @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 ...@@ -9,7 +9,7 @@ import operator
import numbers import numbers
import numpy import numpy
import scipy.constants import scipy.constants
import calcfunctions import functions
from pyparsing import ( from pyparsing import (
Word, Literal, CaselessLiteral, ZeroOrMore, MatchFirst, Optional, Forward, Word, Literal, CaselessLiteral, ZeroOrMore, MatchFirst, Optional, Forward,
...@@ -20,9 +20,9 @@ DEFAULT_FUNCTIONS = { ...@@ -20,9 +20,9 @@ DEFAULT_FUNCTIONS = {
'sin': numpy.sin, 'sin': numpy.sin,
'cos': numpy.cos, 'cos': numpy.cos,
'tan': numpy.tan, 'tan': numpy.tan,
'sec': calcfunctions.sec, 'sec': functions.sec,
'csc': calcfunctions.csc, 'csc': functions.csc,
'cot': calcfunctions.cot, 'cot': functions.cot,
'sqrt': numpy.sqrt, 'sqrt': numpy.sqrt,
'log10': numpy.log10, 'log10': numpy.log10,
'log2': numpy.log2, 'log2': numpy.log2,
...@@ -31,24 +31,24 @@ DEFAULT_FUNCTIONS = { ...@@ -31,24 +31,24 @@ DEFAULT_FUNCTIONS = {
'arccos': numpy.arccos, 'arccos': numpy.arccos,
'arcsin': numpy.arcsin, 'arcsin': numpy.arcsin,
'arctan': numpy.arctan, 'arctan': numpy.arctan,
'arcsec': calcfunctions.arcsec, 'arcsec': functions.arcsec,
'arccsc': calcfunctions.arccsc, 'arccsc': functions.arccsc,
'arccot': calcfunctions.arccot, 'arccot': functions.arccot,
'abs': numpy.abs, 'abs': numpy.abs,
'fact': math.factorial, 'fact': math.factorial,
'factorial': math.factorial, 'factorial': math.factorial,
'sinh': numpy.sinh, 'sinh': numpy.sinh,
'cosh': numpy.cosh, 'cosh': numpy.cosh,
'tanh': numpy.tanh, 'tanh': numpy.tanh,
'sech': calcfunctions.sech, 'sech': functions.sech,
'csch': calcfunctions.csch, 'csch': functions.csch,
'coth': calcfunctions.coth, 'coth': functions.coth,
'arcsinh': numpy.arcsinh, 'arcsinh': numpy.arcsinh,
'arccosh': numpy.arccosh, 'arccosh': numpy.arccosh,
'arctanh': numpy.arctanh, 'arctanh': numpy.arctanh,
'arcsech': calcfunctions.arcsech, 'arcsech': functions.arcsech,
'arccsch': calcfunctions.arccsch, 'arccsch': functions.arccsch,
'arccoth': calcfunctions.arccoth 'arccoth': functions.arccoth
} }
DEFAULT_VARIABLES = { DEFAULT_VARIABLES = {
'i': numpy.complex(0, 1), 'i': numpy.complex(0, 1),
......
...@@ -4,7 +4,7 @@ Unit tests for preview.py ...@@ -4,7 +4,7 @@ Unit tests for preview.py
""" """
import unittest import unittest
import preview from calc import preview
import pyparsing import pyparsing
......
...@@ -2,8 +2,8 @@ from setuptools import setup ...@@ -2,8 +2,8 @@ from setuptools import setup
setup( setup(
name="calc", name="calc",
version="0.1.1", version="0.2",
py_modules=["calc"], packages=["calc"],
install_requires=[ install_requires=[
"pyparsing==1.5.6", "pyparsing==1.5.6",
"numpy", "numpy",
......
...@@ -555,6 +555,13 @@ class LoncapaProblem(object): ...@@ -555,6 +555,13 @@ class LoncapaProblem(object):
Used by get_html. 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') if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')): and 'javascript' in problemtree.get('type')):
# leave javascript intact. # leave javascript intact.
......
...@@ -49,7 +49,7 @@ import pyparsing ...@@ -49,7 +49,7 @@ import pyparsing
from .registry import TagRegistry from .registry import TagRegistry
from chem import chemcalc from chem import chemcalc
from preview import latex_preview from calc.preview import latex_preview
import xqueue_interface import xqueue_interface
from datetime import datetime from datetime import datetime
......
...@@ -915,7 +915,26 @@ class NumericalResponse(LoncapaResponse): ...@@ -915,7 +915,26 @@ class NumericalResponse(LoncapaResponse):
else: else:
return CorrectMap(self.answer_id, 'incorrect') 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): def get_answers(self):
return {self.answer_id: self.correct_answer} return {self.answer_id: self.correct_answer}
...@@ -1778,46 +1797,24 @@ class FormulaResponse(LoncapaResponse): ...@@ -1778,46 +1797,24 @@ class FormulaResponse(LoncapaResponse):
self.correct_answer, given, self.samples) self.correct_answer, given, self.samples)
return CorrectMap(self.answer_id, correctness) return CorrectMap(self.answer_id, correctness)
def check_formula(self, expected, given, samples): def tupleize_answers(self, answer, var_dict_list):
variables = samples.split('@')[0].split(',') """
numsamples = int(samples.split('@')[1].split('#')[1]) Takes in an answer and a list of dictionaries mapping variables to values.
sranges = zip(*map(lambda x: map(float, x.split(",")), Each dictionary represents a test case for the answer.
samples.split('@')[1].split('#')[0].split(':'))) Returns a tuple of formula evaluation results.
"""
ranges = dict(zip(variables, sranges)) out = []
for _ in range(numsamples): for var_dict in var_dict_list:
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
)
try: try:
# log.debug('formula: student_vars=%s, given=%s' % out.append(evaluator(
# (student_variables,given)) var_dict,
dict(),
# Call `evaluator` on the student's answer; look for exceptions answer,
student_result = evaluator( case_sensitive=self.case_sensitive,
student_variables, ))
{},
given,
case_sensitive=self.case_sensitive
)
except UndefinedVariable as uv: except UndefinedVariable as uv:
log.debug( log.debug(
'formularesponse: undefined variable in given=%s', 'formularesponse: undefined variable in formula=%s' % answer)
given
)
raise StudentInputError( raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer" "Invalid input: " + uv.message + " not permitted in answer"
) )
...@@ -1840,17 +1837,70 @@ class FormulaResponse(LoncapaResponse): ...@@ -1840,17 +1837,70 @@ class FormulaResponse(LoncapaResponse):
# If non-factorial related ValueError thrown, handle it the same as any other Exception # If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve)) log.debug('formularesponse: error {0} in formula'.format(ve))
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" % raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given)) cgi.escape(answer))
except Exception as err: except Exception as err:
# traceback.print_exc() # traceback.print_exc()
log.debug('formularesponse: error %s in formula', err) log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" % 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 def randomize_variables(self, samples):
if not compare_with_tolerance(student_result, instructor_result, self.tolerance): """
return "incorrect" Returns a list of dictionaries mapping variables to random values in range,
return "correct" 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): def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word ''' Takes a dict. Returns an identical dict, with all non-word
......
...@@ -16,11 +16,11 @@ __ https://github.com/edx/codejail/blob/master/README.rst ...@@ -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 1. At the instruction to install packages into the sandboxed code, you'll
need to install both `pre-sandbox-requirements.txt` and need to install the requirements from requirements/edx-sandbox::
`sandbox-requirements.txt`::
$ sudo pip install -r pre-sandbox-requirements.txt $ pip install -r requirements/edx-sandbox/base.txt
$ sudo pip install -r sandbox-requirements.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 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 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