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
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches # pylint: disable=E0611
|
||||
from common import type_in_codemirror, press_the_notification_button
|
||||
|
||||
KEY_CSS = '.key input.policy-key'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true, assert_equal, assert_in
|
||||
from nose.tools import assert_true, assert_equal, assert_in # pylint: disable=E0611
|
||||
from terrain.steps import reload_the_page
|
||||
from selenium.common.exceptions import StaleElementReferenceException
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_true # pylint: disable=E0611
|
||||
|
||||
from auth.authz import get_user_by_email, get_course_groupname_for_role
|
||||
from django.conf import settings
|
||||
@@ -265,9 +265,8 @@ def type_in_codemirror(index, text):
|
||||
|
||||
|
||||
def upload_file(filename):
|
||||
file_css = '.upload-dialog input[type=file]'
|
||||
upload = world.css_find(file_css).first
|
||||
path = os.path.join(TEST_ROOT, filename)
|
||||
upload._element.send_keys(os.path.abspath(path))
|
||||
world.browser.execute_script("$('input.file-input').css('display', 'block')")
|
||||
world.browser.attach_file('file', os.path.abspath(path))
|
||||
button_css = '.upload-dialog .action-upload'
|
||||
world.css_click(button_css)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_true # pylint: disable=E0611
|
||||
|
||||
DATA_LOCATION = 'i4x://edx/templates'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world
|
||||
from nose.tools import assert_equal
|
||||
from nose.tools import assert_equal # pylint: disable=E0611
|
||||
from terrain.steps import reload_the_page
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_true, assert_false, assert_equal
|
||||
from nose.tools import assert_true, assert_false, assert_equal # pylint: disable=E0611
|
||||
|
||||
from logging import getLogger
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@@ -7,7 +7,7 @@ from selenium.webdriver.common.keys import Keys
|
||||
from common import type_in_codemirror, upload_file
|
||||
from django.conf import settings
|
||||
|
||||
from nose.tools import assert_true, assert_false, assert_equal
|
||||
from nose.tools import assert_true, assert_false, assert_equal # pylint: disable=E0611
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
|
||||
@@ -168,15 +168,18 @@ def i_see_new_course_image(_step):
|
||||
img = images[0]
|
||||
expected_src = '/c4x/MITx/999/asset/image.jpg'
|
||||
# Don't worry about the domain in the URL
|
||||
assert img['src'].endswith(expected_src)
|
||||
try:
|
||||
assert img['src'].endswith(expected_src)
|
||||
except AssertionError as e:
|
||||
e.args += ('Was looking for {}'.format(expected_src), 'Found {}'.format(img['src']))
|
||||
raise
|
||||
|
||||
|
||||
@step('the image URL should be present in the field')
|
||||
def image_url_present(_step):
|
||||
field_css = '#course-image-url'
|
||||
field = world.css_find(field_css).first
|
||||
expected_value = '/c4x/MITx/999/asset/image.jpg'
|
||||
assert field.value == expected_value
|
||||
assert world.css_value(field_css) == expected_value
|
||||
|
||||
|
||||
############### HELPER METHODS ####################
|
||||
|
||||
@@ -5,7 +5,7 @@ from lettuce import world, step
|
||||
from common import create_studio_user
|
||||
from django.contrib.auth.models import Group
|
||||
from auth.authz import get_course_groupname_for_role, get_user_by_email
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_true # pylint: disable=E0611
|
||||
|
||||
PASSWORD = 'test'
|
||||
EMAIL_EXTENSION = '@edx.org'
|
||||
|
||||
@@ -45,3 +45,25 @@ Feature: Course updates
|
||||
When I modify the handout to "<ol>Test</ol>"
|
||||
Then I see the handout "Test"
|
||||
And I see a "saving" notification
|
||||
|
||||
Scenario: Static links are rewritten when previewing a course update
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
When I add a new update with the text "<img src='/static/my_img.jpg'/>"
|
||||
# Can only do partial text matches because of the quotes with in quotes (and regexp step matching).
|
||||
Then I should see the update "/c4x/MITx/999/asset/my_img.jpg"
|
||||
And I change the update from "/static/my_img.jpg" to "<img src='/static/modified.jpg'/>"
|
||||
Then I should see the update "/c4x/MITx/999/asset/modified.jpg"
|
||||
And when I reload the page
|
||||
Then I should see the update "/c4x/MITx/999/asset/modified.jpg"
|
||||
|
||||
Scenario: Static links are rewritten when previewing handouts
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
When I modify the handout to "<ol><img src='/static/my_img.jpg'/></ol>"
|
||||
# Can only do partial text matches because of the quotes with in quotes (and regexp step matching).
|
||||
Then I see the handout "/c4x/MITx/999/asset/my_img.jpg"
|
||||
And I change the handout from "/static/my_img.jpg" to "<img src='/static/modified.jpg'/>"
|
||||
Then I see the handout "/c4x/MITx/999/asset/modified.jpg"
|
||||
And when I reload the page
|
||||
Then I see the handout "/c4x/MITx/999/asset/modified.jpg"
|
||||
|
||||
@@ -38,6 +38,16 @@ def modify_update(_step, text):
|
||||
change_text(text)
|
||||
|
||||
|
||||
@step(u'I change the update from "([^"]*)" to "([^"]*)"$')
|
||||
def change_existing_update(_step, before, after):
|
||||
verify_text_in_editor_and_update('div.post-preview a.edit-button', before, after)
|
||||
|
||||
|
||||
@step(u'I change the handout from "([^"]*)" to "([^"]*)"$')
|
||||
def change_existing_handout(_step, before, after):
|
||||
verify_text_in_editor_and_update('div.course-handouts a.edit-button', before, after)
|
||||
|
||||
|
||||
@step(u'I delete the update$')
|
||||
def click_button(_step):
|
||||
button_css = 'div.post-preview a.delete-button'
|
||||
@@ -80,3 +90,10 @@ def change_text(text):
|
||||
type_in_codemirror(0, text)
|
||||
save_css = 'a.save-button'
|
||||
world.css_click(save_css)
|
||||
|
||||
|
||||
def verify_text_in_editor_and_update(button_css, before, after):
|
||||
world.css_click(button_css)
|
||||
text = world.css_find(".cm-string").html
|
||||
assert before in text
|
||||
change_text(after)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_equal
|
||||
from nose.tools import assert_equal # pylint: disable=E0611
|
||||
from common import type_in_codemirror
|
||||
|
||||
DISPLAY_NAME = "Display Name"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
from nose.tools import assert_equal # pylint: disable=E0611
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ Feature: Static Pages
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the static pages page
|
||||
And I add a new page
|
||||
When I will confirm all alerts
|
||||
And I "delete" the "Empty" page
|
||||
Then I am shown a prompt
|
||||
When I confirm the prompt
|
||||
Then I should not see a "Empty" static page
|
||||
|
||||
# Safari won't update the name properly
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
from nose.tools import assert_equal
|
||||
from nose.tools import assert_equal # pylint: disable=E0611
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def correct_video_settings(_step):
|
||||
['Download Track', '', False],
|
||||
['Download Video', '', False],
|
||||
['End Time', '0', False],
|
||||
['HTML5 Subtitles', '', False],
|
||||
['HTML5 Timed Transcript', '', False],
|
||||
['Show Captions', 'True', False],
|
||||
['Start Time', '0', False],
|
||||
['Video Sources', '', False],
|
||||
|
||||
@@ -3,11 +3,6 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml_importer import check_module_metadata_editability
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from request_cache.middleware import RequestCache
|
||||
|
||||
from django.core.cache import get_cache
|
||||
|
||||
CACHE = get_cache('mongo_metadata_inheritance')
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Enumerates through the course and find common errors'''
|
||||
@@ -21,12 +16,6 @@ class Command(BaseCommand):
|
||||
loc = CourseDescriptor.id_to_location(loc_str)
|
||||
store = modulestore()
|
||||
|
||||
# setup a request cache so we don't throttle the DB with all the metadata inheritance requests
|
||||
store.set_modulestore_configuration({
|
||||
'metadata_inheritance_cache_subsystem': CACHE,
|
||||
'request_cache': RequestCache.get_request_cache()
|
||||
})
|
||||
|
||||
course = store.get_item(loc, depth=3)
|
||||
|
||||
err_cnt = 0
|
||||
|
||||
@@ -9,14 +9,10 @@ from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from auth.authz import _copy_course_group
|
||||
|
||||
|
||||
#
|
||||
# To run from command line: rake cms:clone SOURCE_LOC=MITx/111/Foo1 DEST_LOC=MITx/135/Foo3
|
||||
#
|
||||
from request_cache.middleware import RequestCache
|
||||
from django.core.cache import get_cache
|
||||
|
||||
CACHE = get_cache('mongo_metadata_inheritance')
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Clone a MongoDB-backed course to another location"""
|
||||
help = 'Clone a MongoDB backed course to another location'
|
||||
@@ -32,11 +28,6 @@ class Command(BaseCommand):
|
||||
mstore = modulestore('direct')
|
||||
cstore = contentstore()
|
||||
|
||||
mstore.set_modulestore_configuration({
|
||||
'metadata_inheritance_cache_subsystem': CACHE,
|
||||
'request_cache': RequestCache.get_request_cache()
|
||||
})
|
||||
|
||||
org, course_num, run = dest_course_id.split("/")
|
||||
mstore.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num))
|
||||
|
||||
|
||||
@@ -9,14 +9,11 @@ from xmodule.course_module import CourseDescriptor
|
||||
from .prompt import query_yes_no
|
||||
|
||||
from auth.authz import _delete_course_group
|
||||
from request_cache.middleware import RequestCache
|
||||
from django.core.cache import get_cache
|
||||
|
||||
|
||||
#
|
||||
# To run from command line: rake cms:delete_course LOC=MITx/111/Foo1
|
||||
#
|
||||
|
||||
CACHE = get_cache('mongo_metadata_inheritance')
|
||||
class Command(BaseCommand):
|
||||
help = '''Delete a MongoDB backed course'''
|
||||
|
||||
@@ -36,11 +33,6 @@ class Command(BaseCommand):
|
||||
ms = modulestore('direct')
|
||||
cs = contentstore()
|
||||
|
||||
ms.set_modulestore_configuration({
|
||||
'metadata_inheritance_cache_subsystem': CACHE,
|
||||
'request_cache': RequestCache.get_request_cache()
|
||||
})
|
||||
|
||||
org, course_num, run = course_id.split("/")
|
||||
ms.ignore_write_events_on_courses.append('{0}/{1}'.format(org, course_num))
|
||||
|
||||
|
||||
8
cms/djangoapps/contentstore/tests/modulestore_config.py
Normal file
8
cms/djangoapps/contentstore/tests/modulestore_config.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Define test configuration for modulestores.
|
||||
"""
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import studio_store_config
|
||||
from django.conf import settings
|
||||
|
||||
TEST_MODULESTORE = studio_store_config(settings.TEST_ROOT / "data")
|
||||
@@ -60,11 +60,11 @@ class UploadTestCase(CourseTestCase):
|
||||
f = BytesIO("sample content")
|
||||
f.name = "sample.txt"
|
||||
resp = self.client.post(self.url, {"name": "my-name", "file": f})
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
def test_no_file(self):
|
||||
resp = self.client.post(self.url, {"name": "file.txt"})
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
def test_get(self):
|
||||
resp = self.client.get(self.url)
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import json
|
||||
import shutil
|
||||
import mock
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
@@ -22,6 +25,7 @@ from contentstore.tests.utils import parse_json
|
||||
from auth.authz import add_user_to_creator_group
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
|
||||
from xmodule.modulestore import Location, mongo
|
||||
@@ -65,7 +69,7 @@ class MongoCollectionFindWrapper(object):
|
||||
return self.original(query, *args, **kwargs)
|
||||
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
|
||||
class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that rely on the toy courses.
|
||||
@@ -312,7 +316,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
|
||||
self.assertIn('/static/', handouts.data)
|
||||
|
||||
def test_import_textbook_as_content_element(self):
|
||||
@mock.patch('xmodule.course_module.requests.get')
|
||||
def test_import_textbook_as_content_element(self, mock_get):
|
||||
mock_get.return_value.text = dedent("""
|
||||
<?xml version="1.0"?><table_of_contents>
|
||||
<entry page="5" page_label="ii" name="Table of Contents"/>
|
||||
</table_of_contents>
|
||||
""").strip()
|
||||
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
@@ -845,7 +856,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
filesystem = OSFS(root_dir / ('test_export/' + dirname))
|
||||
self.assertTrue(filesystem.exists(item.location.name + filename_suffix))
|
||||
|
||||
def test_export_course(self):
|
||||
@mock.patch('xmodule.course_module.requests.get')
|
||||
def test_export_course(self, mock_get):
|
||||
mock_get.return_value.text = dedent("""
|
||||
<?xml version="1.0"?><table_of_contents>
|
||||
<entry page="5" page_label="ii" name="Table of Contents"/>
|
||||
</table_of_contents>
|
||||
""").strip()
|
||||
|
||||
module_store = modulestore('direct')
|
||||
draft_store = modulestore('draft')
|
||||
content_store = contentstore()
|
||||
@@ -1122,12 +1140,15 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
|
||||
wrapper = MongoCollectionFindWrapper(module_store.collection.find)
|
||||
module_store.collection.find = wrapper.find
|
||||
print module_store.metadata_inheritance_cache_subsystem
|
||||
print module_store.request_cache
|
||||
course = module_store.get_item(location, depth=2)
|
||||
|
||||
# make sure we haven't done too many round trips to DB
|
||||
# note we say 4 round trips here for 1) the course, 2 & 3) for the chapters and sequentials, and
|
||||
# 4) because of the RT due to calculating the inherited metadata
|
||||
self.assertEqual(wrapper.counter, 4)
|
||||
# note we say 3 round trips here for 1) the course, and 2 & 3) for the chapters and sequentials
|
||||
# Because we're querying from the top of the tree, we cache information needed for inheritance,
|
||||
# so we don't need to make an extra query to compute it.
|
||||
self.assertEqual(wrapper.counter, 3)
|
||||
|
||||
# make sure we pre-fetched a known sequential which should be at depth=2
|
||||
self.assertTrue(Location(['i4x', 'edX', 'toy', 'sequential',
|
||||
@@ -1163,7 +1184,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
|
||||
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
|
||||
class ContentStoreTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the CMS ContentStore application.
|
||||
@@ -1408,7 +1429,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Chapter 2')
|
||||
|
||||
# go to various pages
|
||||
@@ -1418,92 +1439,92 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# export page
|
||||
resp = self.client.get(reverse('export_course',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# manage users
|
||||
resp = self.client.get(reverse('manage_users',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# course info
|
||||
resp = self.client.get(reverse('course_info',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# settings_details
|
||||
resp = self.client.get(reverse('settings_details',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# settings_details
|
||||
resp = self.client.get(reverse('settings_grading',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# static_pages
|
||||
resp = self.client.get(reverse('static_pages',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'coursename': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# static_pages
|
||||
resp = self.client.get(reverse('asset_index',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# go look at a subsection page
|
||||
subsection_location = loc.replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.get(reverse('edit_subsection',
|
||||
kwargs={'location': subsection_location.url()}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# go look at the Edit page
|
||||
unit_location = loc.replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.get(reverse('edit_unit',
|
||||
kwargs={'location': unit_location.url()}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# delete a component
|
||||
del_loc = loc.replace(category='html', name='test_html')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc.replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc.replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
# delete a chapter
|
||||
del_loc = loc.replace(category='chapter', name='chapter_2')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
def test_import_into_new_course_id(self):
|
||||
module_store = modulestore('direct')
|
||||
@@ -1690,6 +1711,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
content_store.find(location)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class MetadataSaveTestCase(ModuleStoreTestCase):
|
||||
"""Test that metadata is correctly cached and decached."""
|
||||
|
||||
|
||||
@@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
|
||||
def test_get(self):
|
||||
resp = self.client.get(self.url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
|
||||
def test_delete(self):
|
||||
resp = self.client.delete(self.url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
def test_post(self):
|
||||
grader = {
|
||||
@@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
"weight": 17.3,
|
||||
}
|
||||
resp = self.client.post(self.url, grader)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
|
||||
@@ -3,10 +3,13 @@ from unittest import skip
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class InternationalizationTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests to validate Internationalization.
|
||||
|
||||
94
cms/djangoapps/contentstore/tests/test_import_export.py
Normal file
94
cms/djangoapps/contentstore/tests/test_import_export.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
Unit tests for course import and export
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
from pymongo import MongoClient
|
||||
|
||||
from .utils import CourseTestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
class ImportTestCase(CourseTestCase):
|
||||
"""
|
||||
Unit tests for importing a course
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(ImportTestCase, self).setUp()
|
||||
self.url = reverse("import_course", kwargs={
|
||||
'org': self.course.location.org,
|
||||
'course': self.course.location.course,
|
||||
'name': self.course.location.name,
|
||||
})
|
||||
self.content_dir = tempfile.mkdtemp()
|
||||
|
||||
def touch(name):
|
||||
""" Equivalent to shell's 'touch'"""
|
||||
with file(name, 'a'):
|
||||
os.utime(name, None)
|
||||
|
||||
# Create tar test files -----------------------------------------------
|
||||
# OK course:
|
||||
good_dir = tempfile.mkdtemp(dir=self.content_dir)
|
||||
os.makedirs(os.path.join(good_dir, "course"))
|
||||
with open(os.path.join(good_dir, "course.xml"), "w+") as f:
|
||||
f.write('<course url_name="2013_Spring" org="EDx" course="0.00x"/>')
|
||||
|
||||
with open(os.path.join(good_dir, "course", "2013_Spring.xml"), "w+") as f:
|
||||
f.write('<course></course>')
|
||||
|
||||
self.good_tar = os.path.join(self.content_dir, "good.tar.gz")
|
||||
with tarfile.open(self.good_tar, "w:gz") as gtar:
|
||||
gtar.add(good_dir)
|
||||
|
||||
# Bad course (no 'course.xml' file):
|
||||
bad_dir = tempfile.mkdtemp(dir=self.content_dir)
|
||||
touch(os.path.join(bad_dir, "bad.xml"))
|
||||
self.bad_tar = os.path.join(self.content_dir, "bad.tar.gz")
|
||||
with tarfile.open(self.bad_tar, "w:gz") as btar:
|
||||
btar.add(bad_dir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.content_dir)
|
||||
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
|
||||
_CONTENTSTORE.clear()
|
||||
|
||||
def test_no_coursexml(self):
|
||||
"""
|
||||
Check that the response for a tar.gz import without a course.xml is
|
||||
correct.
|
||||
"""
|
||||
with open(self.bad_tar) as btar:
|
||||
resp = self.client.post(
|
||||
self.url,
|
||||
{
|
||||
"name": self.bad_tar,
|
||||
"course-data": [btar]
|
||||
})
|
||||
self.assertEquals(resp.status_code, 415)
|
||||
|
||||
def test_with_coursexml(self):
|
||||
"""
|
||||
Check that the response for a tar.gz import with a course.xml is
|
||||
correct.
|
||||
"""
|
||||
with open(self.good_tar) as gtar:
|
||||
resp = self.client.post(
|
||||
self.url,
|
||||
{
|
||||
"name": self.good_tar,
|
||||
"course-data": [gtar]
|
||||
})
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
@@ -12,24 +12,26 @@ import copy
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from uuid import uuid4
|
||||
|
||||
from pymongo import MongoClient
|
||||
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE, MODULESTORE=TEST_MODULESTORE)
|
||||
class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that rely on the toy and test_import_course courses.
|
||||
@@ -58,6 +60,10 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
def tearDown(self):
|
||||
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
|
||||
_CONTENTSTORE.clear()
|
||||
|
||||
def load_test_import_course(self):
|
||||
'''
|
||||
Load the standard course used to test imports (for do_import_static=False behavior).
|
||||
@@ -121,3 +127,9 @@ class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
|
||||
|
||||
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
|
||||
self.assertIn('/static/', handouts.data)
|
||||
|
||||
def test_tab_name_imports_correctly(self):
|
||||
module_store, content_store, course, course_location = self.load_test_import_course()
|
||||
print "course tabs = {0}".format(course.tabs)
|
||||
self.assertEqual(course.tabs[2]['name'],'Syllabus')
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
|
||||
resp.content,
|
||||
"application/json"
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
|
||||
class TestCreateItem(CourseTestCase):
|
||||
|
||||
@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
def test_view_index(self):
|
||||
"Basic check that the textbook index page responds correctly"
|
||||
resp = self.client.get(self.url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# we don't have resp.context right now,
|
||||
# due to bugs in our testing harness :(
|
||||
if resp.context:
|
||||
@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
self.assertEqual(self.course.pdf_textbooks, obj)
|
||||
|
||||
@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
self.assertEqual(content, obj)
|
||||
|
||||
@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# reload course
|
||||
store = get_modulestore(self.course.location)
|
||||
@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
obj = json.loads(resp.content)
|
||||
self.assertIn("error", obj)
|
||||
|
||||
@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertNotIn("Location", resp)
|
||||
|
||||
|
||||
@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
def test_get_1(self):
|
||||
"Get the first textbook"
|
||||
resp = self.client.get(self.url1)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
compare = json.loads(resp.content)
|
||||
self.assertEqual(compare, self.textbook1)
|
||||
|
||||
def test_get_2(self):
|
||||
"Get the second textbook"
|
||||
resp = self.client.get(self.url2)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
compare = json.loads(resp.content)
|
||||
self.assertEqual(compare, self.textbook2)
|
||||
|
||||
@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
def test_delete(self):
|
||||
"Delete a textbook by ID"
|
||||
resp = self.client.delete(self.url1)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
course = self.store.get_item(self.course.location)
|
||||
self.assertEqual(course.pdf_textbooks, [self.textbook2])
|
||||
|
||||
@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
)
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
resp2 = self.client.get(url)
|
||||
self.assert2XX(resp2.status_code)
|
||||
self.assertEqual(resp2.status_code, 200)
|
||||
compare = json.loads(resp2.content)
|
||||
self.assertEqual(compare, textbook)
|
||||
course = self.store.get_item(self.course.location)
|
||||
@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
)
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
resp2 = self.client.get(self.url2)
|
||||
self.assert2XX(resp2.status_code)
|
||||
self.assertEqual(resp2.status_code, 200)
|
||||
compare = json.loads(resp2.content)
|
||||
self.assertEqual(compare, replacement)
|
||||
course = self.store.get_item(self.course.location)
|
||||
|
||||
@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase):
|
||||
|
||||
def test_detail_inactive(self):
|
||||
resp = self.client.get(self.inactive_detail_url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
result = json.loads(resp.content)
|
||||
self.assertFalse(result["active"])
|
||||
|
||||
def test_detail_invalid(self):
|
||||
resp = self.client.get(self.invalid_detail_url)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
|
||||
@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
data={"role": None},
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
self.assert_not_enrolled()
|
||||
@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
self.assert_not_enrolled()
|
||||
@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase):
|
||||
data={"role": "staff"},
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase):
|
||||
data={"role": "instructor"},
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
|
||||
@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase):
|
||||
data={"role": "instructor"},
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
|
||||
@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase):
|
||||
})
|
||||
|
||||
resp = self.client.delete(self_url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
user = User.objects.get(email=self.user.email)
|
||||
groups = [g.name for g in user.groups.all()]
|
||||
@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.ext_user.save()
|
||||
|
||||
resp = self.client.delete(self.detail_url)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
# reload user from DB
|
||||
@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
self.assert_enrolled()
|
||||
|
||||
def test_staff_to_instructor_still_enrolled(self):
|
||||
@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
self.assert_enrolled()
|
||||
|
||||
def assert_not_enrolled(self):
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.core.cache import cache
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from .utils import parse_json, user, registration
|
||||
from contentstore.tests.utils import parse_json, user, registration
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from contentstore.tests.test_course_settings import CourseTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
import datetime
|
||||
from pytz import UTC
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class ContentStoreTestCase(ModuleStoreTestCase):
|
||||
def _login(self, email, password):
|
||||
"""
|
||||
|
||||
@@ -7,9 +7,11 @@ import json
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from contentstore.tests.modulestore_config import TEST_MODULESTORE
|
||||
|
||||
|
||||
def parse_json(response):
|
||||
@@ -27,6 +29,7 @@ def registration(email):
|
||||
return Registration.objects.get(user__email=email)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_MODULESTORE)
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
|
||||
@@ -10,6 +10,7 @@ from .component import *
|
||||
from .course import *
|
||||
from .error import *
|
||||
from .item import *
|
||||
from .import_export import *
|
||||
from .preview import *
|
||||
from .public import *
|
||||
from .user import *
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import tarfile
|
||||
import shutil
|
||||
import cgi
|
||||
import re
|
||||
from functools import partial
|
||||
from tempfile import mkdtemp
|
||||
from path import path
|
||||
@@ -35,9 +36,7 @@ from .access import get_location_and_verify_access
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
|
||||
__all__ = ['asset_index', 'upload_asset', 'import_course',
|
||||
'generate_export_course', 'export_course']
|
||||
|
||||
__all__ = ['asset_index', 'upload_asset']
|
||||
|
||||
def assets_to_json_dict(assets):
|
||||
"""
|
||||
@@ -167,7 +166,7 @@ def upload_asset(request, org, course, coursename):
|
||||
sc_partial = partial(StaticContent, content_loc, filename, mime_type)
|
||||
if chunked:
|
||||
content = sc_partial(upload_file.chunks())
|
||||
temp_filepath = upload_file.temporary_file_path()
|
||||
tempfile_path = upload_file.temporary_file_path()
|
||||
else:
|
||||
content = sc_partial(upload_file.read())
|
||||
tempfile_path = None
|
||||
@@ -260,179 +259,3 @@ def remove_asset(request, org, course, name):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("GET", "POST", "PUT"))
|
||||
@login_required
|
||||
def import_course(request, org, course, name):
|
||||
"""
|
||||
This method will handle a POST request to upload and import a .tar.gz file into a specified course
|
||||
"""
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
if request.method in ('POST', 'PUT'):
|
||||
filename = request.FILES['course-data'].name
|
||||
|
||||
if not filename.endswith('.tar.gz'):
|
||||
return HttpResponse(json.dumps({'ErrMsg': 'We only support uploading a .tar.gz file.'}))
|
||||
|
||||
data_root = path(settings.GITHUB_REPO_ROOT)
|
||||
|
||||
course_subdir = "{0}-{1}-{2}".format(org, course, name)
|
||||
course_dir = data_root / course_subdir
|
||||
if not course_dir.isdir():
|
||||
os.mkdir(course_dir)
|
||||
|
||||
temp_filepath = course_dir / filename
|
||||
|
||||
logging.debug('importing course to {0}'.format(temp_filepath))
|
||||
|
||||
# stream out the uploaded files in chunks to disk
|
||||
temp_file = open(temp_filepath, 'wb+')
|
||||
for chunk in request.FILES['course-data'].chunks():
|
||||
temp_file.write(chunk)
|
||||
temp_file.close()
|
||||
|
||||
tar_file = tarfile.open(temp_filepath)
|
||||
tar_file.extractall(course_dir + '/')
|
||||
|
||||
# find the 'course.xml' file
|
||||
dirpath = None
|
||||
for dirpath, _dirnames, filenames in os.walk(course_dir):
|
||||
for filename in filenames:
|
||||
if filename == 'course.xml':
|
||||
break
|
||||
if filename == 'course.xml':
|
||||
break
|
||||
|
||||
if filename != 'course.xml':
|
||||
return HttpResponse(json.dumps({'ErrMsg': 'Could not find the course.xml file in the package.'}))
|
||||
|
||||
logging.debug('found course.xml at {0}'.format(dirpath))
|
||||
|
||||
if dirpath != course_dir:
|
||||
for fname in os.listdir(dirpath):
|
||||
shutil.move(dirpath / fname, course_dir)
|
||||
|
||||
_module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
|
||||
[course_subdir], load_error_modules=False,
|
||||
static_content_store=contentstore(),
|
||||
target_location_namespace=location,
|
||||
draft_store=modulestore())
|
||||
|
||||
# we can blow this away when we're done importing.
|
||||
shutil.rmtree(course_dir)
|
||||
|
||||
logging.debug('new course at {0}'.format(course_items[0].location))
|
||||
|
||||
create_all_course_groups(request.user, course_items[0].location)
|
||||
|
||||
logging.debug('created all course groups at {0}'.format(course_items[0].location))
|
||||
|
||||
return HttpResponse(json.dumps({'Status': 'OK'}))
|
||||
else:
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
return render_to_response('import.html', {
|
||||
'context_course': course_module,
|
||||
'successful_import_redirect_url': reverse('course_index', kwargs={
|
||||
'org': location.org,
|
||||
'course': location.course,
|
||||
'name': location.name,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def generate_export_course(request, org, course, name):
|
||||
"""
|
||||
This method will serialize out a course to a .tar.gz file which contains a XML-based representation of
|
||||
the course
|
||||
"""
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
course_module = modulestore().get_instance(location.course_id, location)
|
||||
loc = Location(location)
|
||||
export_file = NamedTemporaryFile(prefix=name + '.', suffix=".tar.gz")
|
||||
|
||||
root_dir = path(mkdtemp())
|
||||
|
||||
try:
|
||||
export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
|
||||
except SerializationError, e:
|
||||
logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
|
||||
|
||||
unit = None
|
||||
failed_item = None
|
||||
parent = None
|
||||
try:
|
||||
failed_item = modulestore().get_instance(course_module.location.course_id, e.location)
|
||||
parent_locs = modulestore().get_parent_locations(failed_item.location, course_module.location.course_id)
|
||||
|
||||
if len(parent_locs) > 0:
|
||||
parent = modulestore().get_item(parent_locs[0])
|
||||
if parent.location.category == 'vertical':
|
||||
unit = parent
|
||||
except:
|
||||
# if we have a nested exception, then we'll show the more generic error message
|
||||
pass
|
||||
|
||||
return render_to_response('export.html', {
|
||||
'context_course': course_module,
|
||||
'successful_import_redirect_url': '',
|
||||
'in_err': True,
|
||||
'raw_err_msg': str(e),
|
||||
'failed_module': failed_item,
|
||||
'unit': unit,
|
||||
'edit_unit_url': reverse('edit_unit', kwargs={
|
||||
'location': parent.location
|
||||
}) if parent else '',
|
||||
'course_home_url': reverse('course_index', kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
'name': name
|
||||
})
|
||||
})
|
||||
except Exception, e:
|
||||
logging.exception('There was an error exporting course {0}. {1}'.format(course_module.location, unicode(e)))
|
||||
return render_to_response('export.html', {
|
||||
'context_course': course_module,
|
||||
'successful_import_redirect_url': '',
|
||||
'in_err': True,
|
||||
'unit': None,
|
||||
'raw_err_msg': str(e),
|
||||
'course_home_url': reverse('course_index', kwargs={
|
||||
'org': org,
|
||||
'course': course,
|
||||
'name': name
|
||||
})
|
||||
})
|
||||
|
||||
logging.debug('tar file being generated at {0}'.format(export_file.name))
|
||||
tar_file = tarfile.open(name=export_file.name, mode='w:gz')
|
||||
tar_file.add(root_dir / name, arcname=name)
|
||||
tar_file.close()
|
||||
|
||||
# remove temp dir
|
||||
shutil.rmtree(root_dir / name)
|
||||
|
||||
wrapper = FileWrapper(export_file)
|
||||
response = HttpResponse(wrapper, content_type='application/x-tgz')
|
||||
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(export_file.name)
|
||||
response['Content-Length'] = os.path.getsize(export_file.name)
|
||||
return response
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def export_course(request, org, course, name):
|
||||
"""
|
||||
This method serves up the 'Export Course' page
|
||||
"""
|
||||
location = get_location_and_verify_access(request, org, course, name)
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
|
||||
return render_to_response('export.html', {
|
||||
'context_course': course_module,
|
||||
'successful_import_redirect_url': ''
|
||||
})
|
||||
|
||||
@@ -18,6 +18,7 @@ from mitxmako.shortcuts import render_to_response
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
from xmodule.modulestore.exceptions import (
|
||||
ItemNotFoundError, InvalidLocationError)
|
||||
@@ -206,7 +207,8 @@ def course_info(request, org, course, name, provided_id=None):
|
||||
'context_course': course_module,
|
||||
'url_base': "/" + org + "/" + course + "/",
|
||||
'course_updates': json.dumps(get_course_updates(location)),
|
||||
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url() })
|
||||
'handouts_location': Location(['i4x', org, course, 'course_info', 'handouts']).url(),
|
||||
'base_asset_url': StaticContent.get_base_url_path_for_course_assets(location) + '/'})
|
||||
|
||||
|
||||
@expect_json
|
||||
|
||||
325
cms/djangoapps/contentstore/views/import_export.py
Normal file
325
cms/djangoapps/contentstore/views/import_export.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
These views handle all actions in Studio related to import and exporting of
|
||||
courses
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import tarfile
|
||||
import shutil
|
||||
import re
|
||||
from tempfile import mkdtemp
|
||||
from path import path
|
||||
from contextlib import contextmanager
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.servers.basehttp import FileWrapper
|
||||
from django.core.files.temp import NamedTemporaryFile
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from auth.authz import create_all_course_groups
|
||||
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.xml_exporter import export_to_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.exceptions import SerializationError
|
||||
|
||||
from .access import get_location_and_verify_access
|
||||
from util.json_request import JsonResponse
|
||||
|
||||
|
||||
__all__ = ['import_course', 'generate_export_course', 'export_course']
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Regex to capture Content-Range header ranges.
|
||||
CONTENT_RE = re.compile(r"(?P<start>\d{1,11})-(?P<stop>\d{1,11})/(?P<end>\d{1,11})")
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@contextmanager
|
||||
def wfile(filename, dirname):
|
||||
"""
|
||||
A with-context that creates `filename` on entry and removes it on exit.
|
||||
`filename` is truncted on creation. Additionally removes dirname on
|
||||
exit.
|
||||
"""
|
||||
open("file", "w").close()
|
||||
try:
|
||||
yield filename
|
||||
finally:
|
||||
os.remove(filename)
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
if request.method == 'POST':
|
||||
|
||||
data_root = path(settings.GITHUB_REPO_ROOT)
|
||||
course_subdir = "{0}-{1}-{2}".format(org, course, name)
|
||||
course_dir = data_root / course_subdir
|
||||
|
||||
filename = request.FILES['course-data'].name
|
||||
if not filename.endswith('.tar.gz'):
|
||||
return JsonResponse(
|
||||
{'ErrMsg': 'We only support uploading a .tar.gz file.'},
|
||||
status=415
|
||||
)
|
||||
temp_filepath = course_dir / filename
|
||||
|
||||
if not course_dir.isdir():
|
||||
os.mkdir(course_dir)
|
||||
|
||||
logging.debug('importing course to {0}'.format(temp_filepath))
|
||||
|
||||
# Get upload chunks byte ranges
|
||||
try:
|
||||
matches = CONTENT_RE.search(request.META["HTTP_CONTENT_RANGE"])
|
||||
content_range = matches.groupdict()
|
||||
except KeyError: # Single chunk
|
||||
# no Content-Range header, so make one that will work
|
||||
content_range = {'start': 0, 'stop': 1, 'end': 2}
|
||||
|
||||
# stream out the uploaded files in chunks to disk
|
||||
if int(content_range['start']) == 0:
|
||||
mode = "wb+"
|
||||
else:
|
||||
mode = "ab+"
|
||||
size = os.path.getsize(temp_filepath)
|
||||
# Check to make sure we haven't missed a chunk
|
||||
# This shouldn't happen, even if different instances are handling
|
||||
# the same session, but it's always better to catch errors earlier.
|
||||
if size < int(content_range['start']):
|
||||
log.warning(
|
||||
"Reported range %s does not match size downloaded so far %s",
|
||||
content_range['start'],
|
||||
size
|
||||
)
|
||||
return JsonResponse(
|
||||
{'ErrMsg': 'File upload corrupted. Please try again'},
|
||||
status=409
|
||||
)
|
||||
# The last request sometimes comes twice. This happens because
|
||||
# nginx sends a 499 error code when the response takes too long.
|
||||
elif size > int(content_range['stop']) and size == int(content_range['end']):
|
||||
return JsonResponse({'ImportStatus': 1})
|
||||
|
||||
with open(temp_filepath, mode) as temp_file:
|
||||
for chunk in request.FILES['course-data'].chunks():
|
||||
temp_file.write(chunk)
|
||||
|
||||
size = os.path.getsize(temp_filepath)
|
||||
|
||||
if int(content_range['stop']) != int(content_range['end']) - 1:
|
||||
# More chunks coming
|
||||
return JsonResponse({
|
||||
"files": [{
|
||||
"name": filename,
|
||||
"size": size,
|
||||
"deleteUrl": "",
|
||||
"deleteType": "",
|
||||
"url": reverse('import_course', kwargs={
|
||||
'org': location.org,
|
||||
'course': location.course,
|
||||
'name': location.name
|
||||
}),
|
||||
"thumbnailUrl": ""
|
||||
}]
|
||||
})
|
||||
|
||||
else: # This was the last chunk.
|
||||
|
||||
# 'Lock' with status info.
|
||||
status_file = data_root / (course + filename + ".lock")
|
||||
|
||||
# Do everything from now on in a with-context, to be sure we've
|
||||
# properly cleaned up.
|
||||
with wfile(status_file, course_dir):
|
||||
|
||||
with open(status_file, 'w+') as sf:
|
||||
sf.write("Extracting")
|
||||
|
||||
tar_file = tarfile.open(temp_filepath)
|
||||
tar_file.extractall(course_dir + '/')
|
||||
|
||||
with open(status_file, 'w+') as sf:
|
||||
sf.write("Verifying")
|
||||
|
||||
# find the 'course.xml' file
|
||||
dirpath = None
|
||||
|
||||
def get_all_files(directory):
|
||||
"""
|
||||
For each file in the directory, yield a 2-tuple of (file-name,
|
||||
directory-path)
|
||||
"""
|
||||
for dirpath, _dirnames, filenames in os.walk(directory):
|
||||
for filename in filenames:
|
||||
yield (filename, dirpath)
|
||||
|
||||
def get_dir_for_fname(directory, filename):
|
||||
"""
|
||||
Returns the dirpath for the first file found in the directory
|
||||
with the given name. If there is no file in the directory with
|
||||
the specified name, return None.
|
||||
"""
|
||||
for fname, dirpath in get_all_files(directory):
|
||||
if fname == filename:
|
||||
return dirpath
|
||||
return None
|
||||
|
||||
fname = "course.xml"
|
||||
|
||||
dirpath = get_dir_for_fname(course_dir, fname)
|
||||
|
||||
if not dirpath:
|
||||
return JsonResponse(
|
||||
{'ErrMsg': 'Could not find the course.xml file in the package.'},
|
||||
status=415
|
||||
)
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
logging.debug('new course at {0}'.format(course_items[0].location))
|
||||
|
||||
with open(status_file, 'w') as sf:
|
||||
sf.write("Updating course")
|
||||
|
||||
create_all_course_groups(request.user, course_items[0].location)
|
||||
logging.debug('created all course groups at {0}'.format(course_items[0].location))
|
||||
|
||||
return JsonResponse({'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': ''
|
||||
})
|
||||
@@ -246,7 +246,7 @@ PIPELINE_JS = {
|
||||
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
|
||||
'js/models/uploads.js', 'js/views/uploads.js',
|
||||
'js/models/textbook.js', 'js/views/textbook.js',
|
||||
'js/views/assets.js', 'js/utility.js',
|
||||
'js/views/assets.js', 'js/src/utility.js',
|
||||
'js/models/settings/course_grading_policy.js'],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
'test_order': 0
|
||||
|
||||
@@ -1,53 +0,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')
|
||||
@@ -1,22 +0,0 @@
|
||||
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)
|
||||
25
cms/startup.py
Normal file
25
cms/startup.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
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()
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
1
cms/static/coffee/fixtures
Symbolic link
1
cms/static/coffee/fixtures
Symbolic link
@@ -0,0 +1 @@
|
||||
../../templates/js/
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/edit-chapter.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/edit-textbook.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/metadata-editor.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/metadata-list-entry.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/metadata-number-entry.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/metadata-option-entry.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/metadata-string-entry.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/no-textbooks.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/section-name-edit.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/show-textbook.underscore
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/system-feedback.underscore
|
||||
@@ -1,33 +0,0 @@
|
||||
<div class="base_wrapper">
|
||||
<section class="editor-with-tabs">
|
||||
<div class="wrapper-comp-editor" id="editor-tab-id" data-html_id='test_id'>
|
||||
<div class="edit-header">
|
||||
<ul class="editor-tabs">
|
||||
<li class="inner_tab_wrap"><a href="#tab-0" class="tab">Tab 0 Editor</a></li>
|
||||
<li class="inner_tab_wrap"><a href="#tab-1" class="tab">Tab 1 Transcripts</a></li>
|
||||
<li class="inner_tab_wrap" id="settings"><a href="#tab-2" class="tab">Tab 2 Settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="component-tab" id="tab-0">
|
||||
<textarea name="" class="edit-box">XML Editor Text</textarea>
|
||||
</div>
|
||||
<div class="component-tab" id="tab-1">
|
||||
Transcripts
|
||||
</div>
|
||||
<div class="component-tab" id="tab-2">
|
||||
Subtitles
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper-comp-settings">
|
||||
<ul>
|
||||
<li id="editor-mode"><a>Editor</a></li>
|
||||
<li id="settings-mode"><a>Settings</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="component-edit-header" style="display: block"/>
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../templates/js/upload-dialog.underscore
|
||||
@@ -1,4 +1,4 @@
|
||||
jasmine.getFixtures().fixturesPath = 'fixtures'
|
||||
jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
|
||||
|
||||
# Stub jQuery.cookie
|
||||
@stubCookies =
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
describe "TabsEditingDescriptor", ->
|
||||
beforeEach ->
|
||||
@isInactiveClass = "is-inactive"
|
||||
@isCurrent = "current"
|
||||
loadFixtures 'tabs-edit.html'
|
||||
@descriptor = new TabsEditingDescriptor($('.base_wrapper'))
|
||||
@html_id = 'test_id'
|
||||
@tab_0_switch = jasmine.createSpy('tab_0_switch');
|
||||
@tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate');
|
||||
@tab_1_switch = jasmine.createSpy('tab_1_switch');
|
||||
@tab_1_modelUpdate = jasmine.createSpy('tab_1_modelUpdate');
|
||||
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 0 Editor', @tab_0_modelUpdate)
|
||||
TabsEditingDescriptor.Model.addOnSwitch(@html_id, 'Tab 0 Editor', @tab_0_switch)
|
||||
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 1 Transcripts', @tab_1_modelUpdate)
|
||||
TabsEditingDescriptor.Model.addOnSwitch(@html_id, 'Tab 1 Transcripts', @tab_1_switch)
|
||||
|
||||
spyOn($.fn, 'hide').andCallThrough()
|
||||
spyOn($.fn, 'show').andCallThrough()
|
||||
spyOn(TabsEditingDescriptor.Model, 'initialize')
|
||||
spyOn(TabsEditingDescriptor.Model, 'updateValue')
|
||||
|
||||
afterEach ->
|
||||
TabsEditingDescriptor.Model.modules= {}
|
||||
|
||||
describe "constructor", ->
|
||||
it "first tab should be visible", ->
|
||||
expect(@descriptor.$tabs.first()).toHaveClass(@isCurrent)
|
||||
expect(@descriptor.$content.first()).not.toHaveClass(@isInactiveClass)
|
||||
|
||||
describe "onSwitchEditor", ->
|
||||
it "switching tabs changes styles", ->
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
expect(@descriptor.$tabs.eq(0)).not.toHaveClass(@isCurrent)
|
||||
expect(@descriptor.$content.eq(0)).toHaveClass(@isInactiveClass)
|
||||
expect(@descriptor.$tabs.eq(1)).toHaveClass(@isCurrent)
|
||||
expect(@descriptor.$content.eq(1)).not.toHaveClass(@isInactiveClass)
|
||||
expect(@tab_1_switch).toHaveBeenCalled()
|
||||
|
||||
it "if click on current tab, nothing should happen", ->
|
||||
spyOn($.fn, 'trigger').andCallThrough()
|
||||
currentTab = @descriptor.$tabs.filter('.' + @isCurrent)
|
||||
@descriptor.$tabs.eq(0).trigger("click")
|
||||
expect(@descriptor.$tabs.filter('.' + @isCurrent)).toEqual(currentTab)
|
||||
expect($.fn.trigger.calls.length).toEqual(1)
|
||||
|
||||
it "onSwitch function call", ->
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
expect(TabsEditingDescriptor.Model.updateValue).toHaveBeenCalled()
|
||||
expect(@tab_1_switch).toHaveBeenCalled()
|
||||
|
||||
describe "save", ->
|
||||
it "function for current tab should be called", ->
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
data = @descriptor.save().data
|
||||
expect(@tab_1_modelUpdate).toHaveBeenCalled()
|
||||
|
||||
it "detach click event", ->
|
||||
spyOn($.fn, "off")
|
||||
@descriptor.save()
|
||||
expect($.fn.off).toHaveBeenCalledWith(
|
||||
'click',
|
||||
'.editor-tabs .tab',
|
||||
@descriptor.onSwitchEditor
|
||||
)
|
||||
|
||||
describe "editor/settings header", ->
|
||||
it "is hidden", ->
|
||||
expect(@descriptor.element.find(".component-edit-header").css('display')).toEqual('none')
|
||||
|
||||
describe "TabsEditingDescriptor special save cases", ->
|
||||
beforeEach ->
|
||||
@isInactiveClass = "is-inactive"
|
||||
@isCurrent = "current"
|
||||
loadFixtures 'tabs-edit.html'
|
||||
@descriptor = new window.TabsEditingDescriptor($('.base_wrapper'))
|
||||
@html_id = 'test_id'
|
||||
|
||||
describe "save", ->
|
||||
it "case: no init", ->
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual(null)
|
||||
|
||||
it "case: no function in model update", ->
|
||||
TabsEditingDescriptor.Model.initialize(@html_id)
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual(null)
|
||||
|
||||
it "case: no function in model update, but value presented", ->
|
||||
@tab_0_modelUpdate = jasmine.createSpy('tab_0_modelUpdate').andReturn(1)
|
||||
TabsEditingDescriptor.Model.addModelUpdate(@html_id, 'Tab 0 Editor', @tab_0_modelUpdate)
|
||||
@descriptor.$tabs.eq(1).trigger("click")
|
||||
expect(@tab_0_modelUpdate).toHaveBeenCalled()
|
||||
data = @descriptor.save().data
|
||||
expect(data).toEqual(1)
|
||||
|
||||
144
cms/static/coffee/spec/views/course_info_spec.coffee
Normal file
144
cms/static/coffee/spec/views/course_info_spec.coffee
Normal file
@@ -0,0 +1,144 @@
|
||||
courseInfoPage = """
|
||||
<div class="course-info-wrapper">
|
||||
<div class="main-column window">
|
||||
<article class="course-updates" id="course-update-view">
|
||||
<ol class="update-list" id="course-update-list"></ol>
|
||||
</article>
|
||||
</div>
|
||||
<div class="sidebar window course-handouts" id="course-handouts-view"></div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
commonSetup = () ->
|
||||
window.analytics = jasmine.createSpyObj('analytics', ['track'])
|
||||
window.course_location_analytics = jasmine.createSpy()
|
||||
window.courseUpdatesXhr = sinon.useFakeXMLHttpRequest()
|
||||
requests = []
|
||||
window.courseUpdatesXhr.onCreate = (xhr) -> requests.push(xhr)
|
||||
return requests
|
||||
|
||||
commonCleanup = () ->
|
||||
window.courseUpdatesXhr.restore()
|
||||
delete window.analytics
|
||||
delete window.course_location_analytics
|
||||
|
||||
describe "Course Updates", ->
|
||||
courseInfoTemplate = readFixtures('course_info_update.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_update-tpl", type: "text/template"}).text(courseInfoTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
|
||||
@collection = new CMS.Models.CourseUpdateCollection()
|
||||
@courseInfoEdit = new CMS.Views.ClassInfoUpdateView({
|
||||
el: $('.course-updates'),
|
||||
collection: @collection,
|
||||
base_asset_url : 'base-asset-url/'
|
||||
})
|
||||
|
||||
@courseInfoEdit.render()
|
||||
|
||||
@event = {
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
|
||||
@createNewUpdate = () ->
|
||||
# Edit button is not in the template under test (it is in parent HTML).
|
||||
# Therefore call onNew directly.
|
||||
@courseInfoEdit.onNew(@event)
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
|
||||
@requests = commonSetup()
|
||||
|
||||
afterEach ->
|
||||
commonCleanup()
|
||||
|
||||
it "does not rewrite links on save", ->
|
||||
# Create a new update, verifying that the model is created
|
||||
# in the collection and save is called.
|
||||
expect(@collection.isEmpty()).toBeTruthy()
|
||||
@courseInfoEdit.onNew(@event)
|
||||
expect(@collection.length).toEqual(1)
|
||||
model = @collection.at(0)
|
||||
spyOn(model, "save").andCallThrough()
|
||||
spyOn(@courseInfoEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
|
||||
# Click the "Save button."
|
||||
@courseInfoEdit.$el.find('.save-button').click()
|
||||
expect(model.save).toHaveBeenCalled()
|
||||
|
||||
# Verify content sent to server does not have rewritten links.
|
||||
contentSaved = JSON.parse(this.requests[0].requestBody).content
|
||||
expect(contentSaved).toEqual('/static/image.jpg')
|
||||
|
||||
it "does rewrite links for preview", ->
|
||||
# Create a new update.
|
||||
@createNewUpdate()
|
||||
|
||||
# Verify the link is rewritten for preview purposes.
|
||||
previewContents = @courseInfoEdit.$el.find('.update-contents').html()
|
||||
expect(previewContents).toEqual('base-asset-url/image.jpg')
|
||||
|
||||
it "shows static links in edit mode", ->
|
||||
@createNewUpdate()
|
||||
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@courseInfoEdit.$el.find('.edit-button').click()
|
||||
expect(@courseInfoEdit.$codeMirror.getValue()).toEqual('/static/image.jpg')
|
||||
|
||||
|
||||
describe "Course Handouts", ->
|
||||
handoutsTemplate = readFixtures('course_info_handouts.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "course_info_handouts-tpl", type: "text/template"}).text(handoutsTemplate))
|
||||
appendSetFixtures courseInfoPage
|
||||
|
||||
@model = new CMS.Models.ModuleInfo({
|
||||
id: 'handouts-id',
|
||||
data: '/static/fromServer.jpg'
|
||||
})
|
||||
|
||||
@handoutsEdit = new CMS.Views.ClassInfoHandoutsView({
|
||||
el: $('#course-handouts-view'),
|
||||
model: @model,
|
||||
base_asset_url: 'base-asset-url/'
|
||||
});
|
||||
|
||||
@handoutsEdit.render()
|
||||
|
||||
@requests = commonSetup()
|
||||
|
||||
afterEach ->
|
||||
commonCleanup()
|
||||
|
||||
it "does not rewrite links on save", ->
|
||||
# Enter something in the handouts section, verifying that the model is saved
|
||||
# when "Save" is clicked.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
spyOn(@model, "save").andCallThrough()
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
expect(@model.save).toHaveBeenCalled()
|
||||
|
||||
contentSaved = JSON.parse(this.requests[0].requestBody).data
|
||||
expect(contentSaved).toEqual('/static/image.jpg')
|
||||
|
||||
it "does rewrite links in initial content", ->
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/fromServer.jpg')
|
||||
|
||||
it "does rewrite links after edit", ->
|
||||
# Edit handouts and save.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
spyOn(@handoutsEdit.$codeMirror, 'getValue').andReturn('/static/image.jpg')
|
||||
@handoutsEdit.$el.find('.save-button').click()
|
||||
|
||||
# Verify preview text.
|
||||
expect(@handoutsEdit.$preview.html().trim()).toBe('base-asset-url/image.jpg')
|
||||
|
||||
it "shows static links in edit mode", ->
|
||||
# Click edit and verify CodeMirror contents.
|
||||
@handoutsEdit.$el.find('.edit-button').click()
|
||||
expect(@handoutsEdit.$codeMirror.getValue().trim()).toEqual('/static/fromServer.jpg')
|
||||
|
||||
@@ -64,20 +64,31 @@ class CMS.Views.TabsEdit extends Backbone.View
|
||||
course: course_location_analytics
|
||||
|
||||
deleteTab: (event) =>
|
||||
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.'
|
||||
return
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
|
||||
analytics.track "Deleted Static Page",
|
||||
course: course_location_analytics
|
||||
id: $component.data('id')
|
||||
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
$component.remove()
|
||||
)
|
||||
|
||||
|
||||
|
||||
confirm = new CMS.Views.Prompt.Warning
|
||||
title: gettext('Delete Component Confirmation')
|
||||
message: gettext('Are you sure you want to delete this component? This action cannot be undone.')
|
||||
actions:
|
||||
primary:
|
||||
text: gettext("OK")
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
|
||||
analytics.track "Deleted Static Page",
|
||||
course: course_location_analytics
|
||||
id: $component.data('id')
|
||||
deleting = new CMS.Views.Notification.Mini
|
||||
title: gettext('Deleting') + '…'
|
||||
deleting.show()
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
}, =>
|
||||
$component.remove()
|
||||
deleting.hide()
|
||||
)
|
||||
secondary: [
|
||||
text: gettext('Cancel')
|
||||
click: (view) ->
|
||||
view.hide()
|
||||
]
|
||||
confirm.show()
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
@font-face{font-family:'Open Sans';font-style:normal;font-weight:700;src:local("Open Sans Bold"),local("OpenSans-Bold"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/k3k702ZOKiLJc3WVjuplzKRDOzjiPcYnFooOUGCOsRk.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:normal;font-weight:300;src:local("Open Sans Light"),local("OpenSans-Light"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/DXI1ORHCpsQm3Vp6mXoaTaRDOzjiPcYnFooOUGCOsRk.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:700;src:local("Open Sans Bold Italic"),local("OpenSans-BoldItalic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxhbnBKKEOwRKgsHDreGcocg.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:300;src:local("Open Sans Light Italic"),local("OpenSansLight-Italic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/PRmiXeptR36kaC0GEAetxvR_54zmj3SbGZQh3vCOwvY.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:italic;font-weight:400;src:local("Open Sans Italic"),local("OpenSans-Italic"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/xjAJXh38I15wypJXxuGMBrrIa-7acMAeDBVuclsi6Gc.woff) format("woff")}@font-face{font-family:'Open Sans';font-style:normal;font-weight:400;src:local("Open Sans"),local("OpenSans"),url(http://themes.googleusercontent.com/static/fonts/opensans/v6/cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff) format("woff")}
|
||||
|
||||
.mceContentBody {
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
font-family: 'Open Sans', Verdana, Arial, Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #3c3c3c;
|
||||
scrollbar-3dlight-color: #F0F0EE;
|
||||
scrollbar-arrow-color: #676662;
|
||||
scrollbar-base-color: #F0F0EE;
|
||||
scrollbar-darkshadow-color: #DDDDDD;
|
||||
scrollbar-face-color: #E0E0DD;
|
||||
scrollbar-highlight-color: #F0F0EE;
|
||||
scrollbar-shadow-color: #F0F0EE;
|
||||
scrollbar-track-color: #F5F5F5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #3c3c3c;
|
||||
font-weight: normal;
|
||||
font-size: 2em;
|
||||
line-height: 1.4em;
|
||||
letter-spacing: 1px;
|
||||
margin: 0 0 1.416em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #646464;
|
||||
font-weight: normal;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.2em;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 15px;
|
||||
text-transform: uppercase;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: .83em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1.416em;
|
||||
font-size: 1em;
|
||||
line-height: 1.6em !important;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-style: bold;
|
||||
}
|
||||
|
||||
p + p, ul + p, ol + p {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin: 1em 0;
|
||||
padding: 0 0 0 1em;
|
||||
color: #3c3c3c;
|
||||
|
||||
}
|
||||
|
||||
ol li, ul li {
|
||||
margin-bottom: 0.708em;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal outside none;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none;
|
||||
}
|
||||
|
||||
a, a:link, a:visited, a:hover, a:active {
|
||||
color: #1d9dd9;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
color: #3c3c3c;
|
||||
font-family: monospace, serif;
|
||||
font-size: 1em;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace, serif;
|
||||
background: none;
|
||||
color: #3c3c3c;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #eee;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table td, th {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc !important;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -3,6 +3,26 @@
|
||||
The render here adds views for each update/handout by delegating to their collections but does not
|
||||
generate any html for the surrounding page.
|
||||
*/
|
||||
|
||||
var editWithCodeMirror = function(model, contentName, baseAssetUrl, textArea) {
|
||||
var content = rewriteStaticLinks(model.get(contentName), baseAssetUrl, '/static/');
|
||||
model.set(contentName, content);
|
||||
var $codeMirror = CodeMirror.fromTextArea(textArea, {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true
|
||||
});
|
||||
$codeMirror.setValue(content);
|
||||
$codeMirror.clearHistory();
|
||||
return $codeMirror;
|
||||
};
|
||||
|
||||
var changeContentToPreview = function (model, contentName, baseAssetUrl) {
|
||||
var content = rewriteStaticLinks(model.get(contentName), '/static/', baseAssetUrl);
|
||||
model.set(contentName, content);
|
||||
return content;
|
||||
};
|
||||
|
||||
CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
// takes CMS.Models.CourseInfo as model
|
||||
tagName: 'div',
|
||||
@@ -11,18 +31,19 @@ CMS.Views.CourseInfoEdit = Backbone.View.extend({
|
||||
// instantiate the ClassInfoUpdateView and delegate the proper dom to it
|
||||
new CMS.Views.ClassInfoUpdateView({
|
||||
el: $('body.updates'),
|
||||
collection: this.model.get('updates')
|
||||
collection: this.model.get('updates'),
|
||||
base_asset_url: this.model.get('base_asset_url')
|
||||
});
|
||||
|
||||
new CMS.Views.ClassInfoHandoutsView({
|
||||
el: this.$('#course-handouts-view'),
|
||||
model: this.model.get('handouts')
|
||||
model: this.model.get('handouts'),
|
||||
base_asset_url: this.model.get('base_asset_url')
|
||||
});
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
// ??? Programming style question: should each of these classes be in separate files?
|
||||
CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
// collection is CourseUpdateCollection
|
||||
events: {
|
||||
@@ -48,6 +69,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
var self = this;
|
||||
this.collection.each(function (update) {
|
||||
try {
|
||||
changeContentToPreview(update, 'content', self.options['base_asset_url'])
|
||||
var newEle = self.template({ updateModel : update });
|
||||
$(updateEle).append(newEle);
|
||||
} catch (e) {
|
||||
@@ -72,20 +94,18 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
$(updateEle).prepend($newForm);
|
||||
|
||||
var $textArea = $newForm.find(".new-update-content").first();
|
||||
if (this.$codeMirror == null ) {
|
||||
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
});
|
||||
}
|
||||
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true
|
||||
});
|
||||
|
||||
$newForm.addClass('editing');
|
||||
this.$currentPost = $newForm.closest('li');
|
||||
|
||||
window.$modalCover.show();
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor(self, true);
|
||||
self.closeEditor(true);
|
||||
});
|
||||
|
||||
$('.date').datepicker('destroy');
|
||||
@@ -110,7 +130,7 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
ele.remove();
|
||||
}
|
||||
});
|
||||
this.closeEditor(this);
|
||||
this.closeEditor();
|
||||
|
||||
analytics.track('Saved Course Update', {
|
||||
'course': course_location_analytics,
|
||||
@@ -122,8 +142,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
event.preventDefault();
|
||||
// change editor contents back to model values and hide the editor
|
||||
$(this.editor(event)).hide();
|
||||
// If the model was never created (user created a new update, then pressed Cancel),
|
||||
// we wish to remove it from the DOM.
|
||||
var targetModel = this.eventModel(event);
|
||||
this.closeEditor(this, !targetModel.id);
|
||||
this.closeEditor(!targetModel.id);
|
||||
},
|
||||
|
||||
onEdit: function(event) {
|
||||
@@ -134,16 +156,10 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
|
||||
$(this.editor(event)).show();
|
||||
var $textArea = this.$currentPost.find(".new-update-content").first();
|
||||
if (this.$codeMirror == null ) {
|
||||
this.$codeMirror = CodeMirror.fromTextArea($textArea.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
});
|
||||
}
|
||||
var targetModel = this.eventModel(event);
|
||||
this.$codeMirror = editWithCodeMirror(targetModel, 'content', self.options['base_asset_url'], $textArea.get(0));
|
||||
|
||||
window.$modalCover.show();
|
||||
var targetModel = this.eventModel(event);
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor(self);
|
||||
});
|
||||
@@ -193,31 +209,35 @@ CMS.Views.ClassInfoUpdateView = Backbone.View.extend({
|
||||
}
|
||||
});
|
||||
confirm.show();
|
||||
},
|
||||
},
|
||||
|
||||
closeEditor: function(self, removePost) {
|
||||
var targetModel = self.collection.get(self.$currentPost.attr('name'));
|
||||
closeEditor: function(removePost) {
|
||||
var targetModel = this.collection.get(this.$currentPost.attr('name'));
|
||||
|
||||
if(removePost) {
|
||||
self.$currentPost.remove();
|
||||
this.$currentPost.remove();
|
||||
}
|
||||
else {
|
||||
// close the modal and insert the appropriate data
|
||||
this.$currentPost.removeClass('editing');
|
||||
this.$currentPost.find('.date-display').html(targetModel.get('date'));
|
||||
this.$currentPost.find('.date').val(targetModel.get('date'));
|
||||
|
||||
var content = changeContentToPreview(targetModel, 'content', this.options['base_asset_url'])
|
||||
try {
|
||||
// just in case the content causes an error (embedded js errors)
|
||||
this.$currentPost.find('.update-contents').html(content);
|
||||
this.$currentPost.find('.new-update-content').val(content);
|
||||
} catch (e) {
|
||||
// ignore but handle rest of page
|
||||
}
|
||||
this.$currentPost.find('form').hide();
|
||||
this.$currentPost.find('.CodeMirror').remove();
|
||||
}
|
||||
|
||||
// close the modal and insert the appropriate data
|
||||
self.$currentPost.removeClass('editing');
|
||||
self.$currentPost.find('.date-display').html(targetModel.get('date'));
|
||||
self.$currentPost.find('.date').val(targetModel.get('date'));
|
||||
try {
|
||||
// just in case the content causes an error (embedded js errors)
|
||||
self.$currentPost.find('.update-contents').html(targetModel.get('content'));
|
||||
self.$currentPost.find('.new-update-content').val(targetModel.get('content'));
|
||||
} catch (e) {
|
||||
// ignore but handle rest of page
|
||||
}
|
||||
self.$currentPost.find('form').hide();
|
||||
window.$modalCover.unbind('click');
|
||||
window.$modalCover.hide();
|
||||
this.$codeMirror = null;
|
||||
self.$currentPost.find('.CodeMirror').remove();
|
||||
},
|
||||
|
||||
// Dereferencing from events to screen elements
|
||||
@@ -275,8 +295,8 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var updateEle = this.$el;
|
||||
var self = this;
|
||||
changeContentToPreview(this.model, 'data', this.options['base_asset_url'])
|
||||
|
||||
this.$el.html(
|
||||
$(this.template( {
|
||||
model: this.model
|
||||
@@ -295,22 +315,17 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
var self = this;
|
||||
this.$editor.val(this.$preview.html());
|
||||
this.$form.show();
|
||||
if (this.$codeMirror == null) {
|
||||
this.$codeMirror = CodeMirror.fromTextArea(this.$editor.get(0), {
|
||||
mode: "text/html",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.$codeMirror = editWithCodeMirror(self.model, 'data', self.options['base_asset_url'], this.$editor.get(0));
|
||||
|
||||
window.$modalCover.show();
|
||||
window.$modalCover.bind('click', function() {
|
||||
self.closeEditor(self);
|
||||
self.closeEditor();
|
||||
});
|
||||
},
|
||||
|
||||
onSave: function(event) {
|
||||
this.model.set('data', this.$codeMirror.getValue());
|
||||
this.render();
|
||||
var saving = new CMS.Views.Notification.Mini({
|
||||
title: gettext('Saving') + '…'
|
||||
});
|
||||
@@ -320,8 +335,9 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
saving.hide();
|
||||
}
|
||||
});
|
||||
this.render();
|
||||
this.$form.hide();
|
||||
this.closeEditor(this);
|
||||
this.closeEditor();
|
||||
|
||||
analytics.track('Saved Course Handouts', {
|
||||
'course': course_location_analytics
|
||||
@@ -331,14 +347,14 @@ CMS.Views.ClassInfoHandoutsView = Backbone.View.extend({
|
||||
|
||||
onCancel: function(event) {
|
||||
this.$form.hide();
|
||||
this.closeEditor(this);
|
||||
this.closeEditor();
|
||||
},
|
||||
|
||||
closeEditor: function(self) {
|
||||
closeEditor: function() {
|
||||
this.$form.hide();
|
||||
window.$modalCover.unbind('click');
|
||||
window.$modalCover.hide();
|
||||
self.$form.find('.CodeMirror').remove();
|
||||
this.$form.find('.CodeMirror').remove();
|
||||
this.$codeMirror = null;
|
||||
}
|
||||
});
|
||||
|
||||
98
cms/static/js_test.yml
Normal file
98
cms/static/js_test.yml
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
# 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
|
||||
5
cms/static/sass/views/_import.scss
vendored
5
cms/static/sass/views/_import.scss
vendored
@@ -57,6 +57,11 @@ body.course.import {
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.status-block {
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.choose-file-button {
|
||||
@include blue-button;
|
||||
padding: 10px 50px 11px;
|
||||
|
||||
1
cms/static/xmodule_js
Symbolic link
1
cms/static/xmodule_js
Symbolic link
@@ -0,0 +1 @@
|
||||
../../common/lib/xmodule/xmodule/js/
|
||||
@@ -39,6 +39,7 @@
|
||||
model : new CMS.Models.CourseInfo({
|
||||
courseId : '${context_course.location}',
|
||||
updates : course_updates,
|
||||
base_asset_url : '${base_asset_url}',
|
||||
handouts : course_handouts
|
||||
})
|
||||
});
|
||||
|
||||
@@ -25,13 +25,16 @@
|
||||
<p>${_("File uploads must be gzipped tar files (.tar.gz) containing, at a minimum, a {filename} file.").format(filename='<code>course.xml</code>')}</p>
|
||||
<p>${_("Please note that if your course has any problems with auto-generated {nodename} nodes, re-importing your course could cause the loss of student data associated with those problems.").format(nodename='<code>url_name</code>')}</p>
|
||||
</div>
|
||||
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
|
||||
<form id="fileupload" method="post" enctype="multipart/form-data"
|
||||
class="import-form" url="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}">
|
||||
<h2>${_("Course to import:")}</h2>
|
||||
<p class="error-block"></p>
|
||||
<a href="#" class="choose-file-button">${_("Choose File")}</a>
|
||||
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">${_("change")}</a></p>
|
||||
<input type="file" name="course-data" class="file-input">
|
||||
<input type="submit" value="${_('Replace my course with the one above')}" class="submit-button">
|
||||
<input type="file" name="course-data" class="file-input" >
|
||||
<input type="submit" value="${_('Replace my course with the one above')}" class="submit-button" >
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
|
||||
<p class="status-block">Unpacking...</p>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill"></div>
|
||||
<div class="percent">0%</div>
|
||||
@@ -43,6 +46,9 @@
|
||||
</%block>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.iframe-transport.js')}"> </script>
|
||||
<script src="${static.url('js/vendor/jQuery-File-Upload/js/jquery.fileupload.js')}"> </script>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
@@ -50,33 +56,61 @@ var bar = $('.progress-bar');
|
||||
var fill = $('.progress-fill');
|
||||
var percent = $('.percent');
|
||||
var status = $('#status');
|
||||
var statusBlock = $('.status-block');
|
||||
var submitBtn = $('.submit-button');
|
||||
|
||||
$('form').ajaxForm({
|
||||
beforeSend: function() {
|
||||
status.empty();
|
||||
var percentVal = '0%';
|
||||
|
||||
$('#fileupload').fileupload({
|
||||
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
|
||||
maxChunkSize: 20 * 1000000, // 20 MB
|
||||
|
||||
autoUpload: false,
|
||||
|
||||
add: function(e, data) {
|
||||
submitBtn.unbind('click');
|
||||
var file = data.files[0];
|
||||
if (file.type == "application/x-gzip") {
|
||||
submitBtn.click(function(e){
|
||||
e.preventDefault();
|
||||
submitBtn.hide();
|
||||
data.submit().complete(function(result, textStatus, xhr) {
|
||||
if (result.status != 200) {
|
||||
alert('${_("Your import has failed.")}\n\n' + JSON.parse(result.responseText)["ErrMsg"]);
|
||||
submitBtn.show();
|
||||
bar.hide();
|
||||
} else {
|
||||
if (result.responseText["ImportStatus"] == 1) {
|
||||
bar.hide();
|
||||
statusBlock.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
data.files = [];
|
||||
}
|
||||
},
|
||||
|
||||
progressall: function(e, data){
|
||||
var percentVal = parseInt(data.loaded / data.total * 100, 10) + "%";
|
||||
bar.show();
|
||||
fill.width(percentVal);
|
||||
percent.html(percentVal);
|
||||
submitBtn.hide();
|
||||
},
|
||||
uploadProgress: function(event, position, total, percentComplete) {
|
||||
var percentVal = percentComplete + '%';
|
||||
fill.width(percentVal);
|
||||
percent.html(percentVal);
|
||||
},
|
||||
complete: function(xhr) {
|
||||
if (xhr.status == 200) {
|
||||
done: function(e, data){
|
||||
bar.hide();
|
||||
alert('${_("Your import was successful.")}');
|
||||
window.location = '${successful_import_redirect_url}';
|
||||
}
|
||||
else
|
||||
alert('${_("Your import has failed.")}\n\n' + xhr.responseText);
|
||||
submitBtn.show();
|
||||
bar.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
sequentialUploads: true
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
10
cms/urls.py
10
cms/urls.py
@@ -1,9 +1,9 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, include, url
|
||||
|
||||
# Import this file so it can do its work, even though we don't use the name.
|
||||
# pylint: disable=W0611
|
||||
from . import one_time_startup
|
||||
# TODO: This should be removed once the CMS is running via wsgi on all production servers
|
||||
import cms.startup as startup
|
||||
startup.run()
|
||||
|
||||
# There is a course creators admin table.
|
||||
from ratelimitbackend import admin
|
||||
@@ -135,10 +135,6 @@ urlpatterns += (
|
||||
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
|
||||
)
|
||||
|
||||
|
||||
if settings.ENABLE_JASMINE:
|
||||
urlpatterns += (url(r'^_jasmine/', include('django_jasmine.urls')),)
|
||||
|
||||
if settings.MITX_FEATURES.get('ENABLE_SERVICE_STATUS'):
|
||||
urlpatterns += (
|
||||
url(r'^status/', include('service_status.urls')),
|
||||
|
||||
12
cms/wsgi.py
Normal file
12
cms/wsgi.py
Normal file
@@ -0,0 +1,12 @@
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user