diff --git a/.gitignore b/.gitignore
index 4fd90cfe03..72de96e0c4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,4 @@ autodeploy.properties
.ws_migrations_complete
.vagrant/
logs
+.testids/
diff --git a/.pep8 b/.pep8
index 25d0edbcb4..badb0219a0 100644
--- a/.pep8
+++ b/.pep8
@@ -1,2 +1,3 @@
[pep8]
-ignore=E501
\ No newline at end of file
+ignore=E501
+exclude=migrations
\ No newline at end of file
diff --git a/AUTHORS b/AUTHORS
index 4b57d723d2..0391bd55f9 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -84,3 +84,5 @@ Mukul Goyal
Robert Marks
Yarko Tymciurak
Miles Steele
+Kevin Luo
+Akshay Jagadeesh
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 749b9ef56e..89f084b3f4 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,9 +5,15 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
+LMS: Added alphabetical sorting of forum categories and subcategories.
+It is hidden behind a false defaulted course level flag.
+
Studio: Allow course authors to set their course image on the schedule
and details page, with support for JPEG and PNG images.
+LMS, Studio: Centralized startup code to manage.py and wsgi.py files.
+Made studio runnable using wsgi.
+
Blades: Took videoalpha out of alpha, replacing the old video player
Common: Allow instructors to input complicated expressions as answers to
@@ -27,6 +33,9 @@ logic has been consolidated into the model -- you should use new class methods
to `enroll()`, `unenroll()`, and to check `is_enrolled()`, instead of creating
CourseEnrollment objects or querying them directly.
+LMS: Added bulk email for course feature, with option to optout of individual
+course emails.
+
Studio: Email will be sent to admin address when a user requests course creator
privileges for Studio (edge only).
diff --git a/cms/djangoapps/contentstore/features/advanced-settings.py b/cms/djangoapps/contentstore/features/advanced-settings.py
index 18e179abdb..201ac49e52 100644
--- a/cms/djangoapps/contentstore/features/advanced-settings.py
+++ b/cms/djangoapps/contentstore/features/advanced-settings.py
@@ -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'
diff --git a/cms/djangoapps/contentstore/features/checklists.py b/cms/djangoapps/contentstore/features/checklists.py
index e8dcd755a3..1c41eed4d3 100644
--- a/cms/djangoapps/contentstore/features/checklists.py
+++ b/cms/djangoapps/contentstore/features/checklists.py
@@ -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
diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py
index 5d6fde47c8..a6f22db340 100644
--- a/cms/djangoapps/contentstore/features/common.py
+++ b/cms/djangoapps/contentstore/features/common.py
@@ -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)
diff --git a/cms/djangoapps/contentstore/features/component.py b/cms/djangoapps/contentstore/features/component.py
index 15727dd992..d0c1fd59e7 100644
--- a/cms/djangoapps/contentstore/features/component.py
+++ b/cms/djangoapps/contentstore/features/component.py
@@ -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'
diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
index 606e3dcee8..2971085081 100644
--- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
+++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py
@@ -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
diff --git a/cms/djangoapps/contentstore/features/course-overview.py b/cms/djangoapps/contentstore/features/course-overview.py
index 3fcb134f5b..289dbec308 100644
--- a/cms/djangoapps/contentstore/features/course-overview.py
+++ b/cms/djangoapps/contentstore/features/course-overview.py
@@ -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__)
diff --git a/cms/djangoapps/contentstore/features/course-settings.py b/cms/djangoapps/contentstore/features/course-settings.py
index 570c49a8c4..7004b9f99e 100644
--- a/cms/djangoapps/contentstore/features/course-settings.py
+++ b/cms/djangoapps/contentstore/features/course-settings.py
@@ -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 ####################
diff --git a/cms/djangoapps/contentstore/features/course-team.py b/cms/djangoapps/contentstore/features/course-team.py
index 8b31d325e5..85044dbbad 100644
--- a/cms/djangoapps/contentstore/features/course-team.py
+++ b/cms/djangoapps/contentstore/features/course-team.py
@@ -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'
diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature
index 41ee785db5..bc73479c5f 100644
--- a/cms/djangoapps/contentstore/features/course-updates.feature
+++ b/cms/djangoapps/contentstore/features/course-updates.feature
@@ -45,3 +45,25 @@ Feature: Course updates
When I modify the handout to "Test"
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 ""
+ # 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 ""
+ 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 ""
+ # 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 ""
+ 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"
diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py
index f431af9cf5..3278805a48 100644
--- a/cms/djangoapps/contentstore/features/course-updates.py
+++ b/cms/djangoapps/contentstore/features/course-updates.py
@@ -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)
diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py
index d891789e4a..5e4fe6364d 100644
--- a/cms/djangoapps/contentstore/features/problem-editor.py
+++ b/cms/djangoapps/contentstore/features/problem-editor.py
@@ -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"
diff --git a/cms/djangoapps/contentstore/features/section.py b/cms/djangoapps/contentstore/features/section.py
index 3ca8e1676d..3fea8637c6 100644
--- a/cms/djangoapps/contentstore/features/section.py
+++ b/cms/djangoapps/contentstore/features/section.py
@@ -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 ####################
diff --git a/cms/djangoapps/contentstore/features/static-pages.feature b/cms/djangoapps/contentstore/features/static-pages.feature
index c1a8ec91fc..67652ea8f1 100644
--- a/cms/djangoapps/contentstore/features/static-pages.feature
+++ b/cms/djangoapps/contentstore/features/static-pages.feature
@@ -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
diff --git a/cms/djangoapps/contentstore/features/subsection.py b/cms/djangoapps/contentstore/features/subsection.py
index 60a325f550..6d9612d9bd 100644
--- a/cms/djangoapps/contentstore/features/subsection.py
+++ b/cms/djangoapps/contentstore/features/subsection.py
@@ -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 ####################
diff --git a/cms/djangoapps/contentstore/features/video-editor.py b/cms/djangoapps/contentstore/features/video-editor.py
index 312e2d545f..93d3be2ac0 100644
--- a/cms/djangoapps/contentstore/features/video-editor.py
+++ b/cms/djangoapps/contentstore/features/video-editor.py
@@ -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],
diff --git a/cms/djangoapps/contentstore/management/commands/check_course.py b/cms/djangoapps/contentstore/management/commands/check_course.py
index 2f0b0b2a2c..13ac6af50c 100644
--- a/cms/djangoapps/contentstore/management/commands/check_course.py
+++ b/cms/djangoapps/contentstore/management/commands/check_course.py
@@ -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
diff --git a/cms/djangoapps/contentstore/management/commands/clone_course.py b/cms/djangoapps/contentstore/management/commands/clone_course.py
index aa0e076f08..5ad0da09d8 100644
--- a/cms/djangoapps/contentstore/management/commands/clone_course.py
+++ b/cms/djangoapps/contentstore/management/commands/clone_course.py
@@ -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))
diff --git a/cms/djangoapps/contentstore/management/commands/delete_course.py b/cms/djangoapps/contentstore/management/commands/delete_course.py
index b0901ccfc9..50f9b82e80 100644
--- a/cms/djangoapps/contentstore/management/commands/delete_course.py
+++ b/cms/djangoapps/contentstore/management/commands/delete_course.py
@@ -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))
diff --git a/cms/djangoapps/contentstore/tests/modulestore_config.py b/cms/djangoapps/contentstore/tests/modulestore_config.py
new file mode 100644
index 0000000000..234fa66f9f
--- /dev/null
+++ b/cms/djangoapps/contentstore/tests/modulestore_config.py
@@ -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")
diff --git a/cms/djangoapps/contentstore/tests/test_assets.py b/cms/djangoapps/contentstore/tests/test_assets.py
index b627237729..2f158cfda6 100644
--- a/cms/djangoapps/contentstore/tests/test_assets.py
+++ b/cms/djangoapps/contentstore/tests/test_assets.py
@@ -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)
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index f03ee3b81a..696b60fbe5 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -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("""
+
+
+
+ """).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("""
+
+
+
+ """).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."""
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index dbdf8b3f6e..524dde07e5 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -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)
diff --git a/cms/djangoapps/contentstore/tests/test_i18n.py b/cms/djangoapps/contentstore/tests/test_i18n.py
index e6baf57213..9e7a2df8b2 100644
--- a/cms/djangoapps/contentstore/tests/test_i18n.py
+++ b/cms/djangoapps/contentstore/tests/test_i18n.py
@@ -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.
diff --git a/cms/djangoapps/contentstore/tests/test_import_export.py b/cms/djangoapps/contentstore/tests/test_import_export.py
new file mode 100644
index 0000000000..05f2b0b7b9
--- /dev/null
+++ b/cms/djangoapps/contentstore/tests/test_import_export.py
@@ -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('')
+
+ with open(os.path.join(good_dir, "course", "2013_Spring.xml"), "w+") as f:
+ f.write('')
+
+ 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)
diff --git a/cms/djangoapps/contentstore/tests/test_import_nostatic.py b/cms/djangoapps/contentstore/tests/test_import_nostatic.py
index aad6ffbfe4..f0f65c9b07 100644
--- a/cms/djangoapps/contentstore/tests/test_import_nostatic.py
+++ b/cms/djangoapps/contentstore/tests/test_import_nostatic.py
@@ -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')
+
diff --git a/cms/djangoapps/contentstore/tests/test_item.py b/cms/djangoapps/contentstore/tests/test_item.py
index 260444a8f7..e5ff992cb8 100644
--- a/cms/djangoapps/contentstore/tests/test_item.py
+++ b/cms/djangoapps/contentstore/tests/test_item.py
@@ -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):
diff --git a/cms/djangoapps/contentstore/tests/test_textbooks.py b/cms/djangoapps/contentstore/tests/test_textbooks.py
index a21a1b1023..950d0f780e 100644
--- a/cms/djangoapps/contentstore/tests/test_textbooks.py
+++ b/cms/djangoapps/contentstore/tests/test_textbooks.py
@@ -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)
diff --git a/cms/djangoapps/contentstore/tests/test_users.py b/cms/djangoapps/contentstore/tests/test_users.py
index cbb8aa8b01..80b2364c43 100644
--- a/cms/djangoapps/contentstore/tests/test_users.py
+++ b/cms/djangoapps/contentstore/tests/test_users.py
@@ -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):
diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index 0cbc82cbf1..eddf5ab25a 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -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):
"""
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
index a3f211a703..8b3f4cf4b1 100644
--- a/cms/djangoapps/contentstore/tests/utils.py
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -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):
"""
diff --git a/cms/djangoapps/contentstore/views/__init__.py b/cms/djangoapps/contentstore/views/__init__.py
index 197c54ff36..10f6fb79a7 100644
--- a/cms/djangoapps/contentstore/views/__init__.py
+++ b/cms/djangoapps/contentstore/views/__init__.py
@@ -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 *
diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py
index 74cb94a354..4743622fa8 100644
--- a/cms/djangoapps/contentstore/views/assets.py
+++ b/cms/djangoapps/contentstore/views/assets.py
@@ -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': ''
- })
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index aad56e4a2e..939286a765 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -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
diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py
new file mode 100644
index 0000000000..5830e07a52
--- /dev/null
+++ b/cms/djangoapps/contentstore/views/import_export.py
@@ -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\d{1,11})-(?P\d{1,11})/(?P\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': ''
+ })
diff --git a/cms/envs/common.py b/cms/envs/common.py
index a06d5a36e1..4421474287 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -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
diff --git a/cms/envs/jasmine.py b/cms/envs/jasmine.py
deleted file mode 100644
index a4b8292d71..0000000000
--- a/cms/envs/jasmine.py
+++ /dev/null
@@ -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')
diff --git a/cms/one_time_startup.py b/cms/one_time_startup.py
deleted file mode 100644
index 4198cf2637..0000000000
--- a/cms/one_time_startup.py
+++ /dev/null
@@ -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)
diff --git a/cms/startup.py b/cms/startup.py
new file mode 100644
index 0000000000..eb1098a707
--- /dev/null
+++ b/cms/startup.py
@@ -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()
+
diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json
deleted file mode 100644
index 3964bee455..0000000000
--- a/cms/static/coffee/files.json
+++ /dev/null
@@ -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"
- ]
-}
diff --git a/cms/static/coffee/fixtures b/cms/static/coffee/fixtures
new file mode 120000
index 0000000000..800ce1d60d
--- /dev/null
+++ b/cms/static/coffee/fixtures
@@ -0,0 +1 @@
+../../templates/js/
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/edit-chapter.underscore b/cms/static/coffee/fixtures/edit-chapter.underscore
deleted file mode 120000
index 9e057ab233..0000000000
--- a/cms/static/coffee/fixtures/edit-chapter.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/edit-chapter.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/edit-textbook.underscore b/cms/static/coffee/fixtures/edit-textbook.underscore
deleted file mode 120000
index 5bb17a2d43..0000000000
--- a/cms/static/coffee/fixtures/edit-textbook.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/edit-textbook.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/metadata-editor.underscore b/cms/static/coffee/fixtures/metadata-editor.underscore
deleted file mode 120000
index 9696774d0a..0000000000
--- a/cms/static/coffee/fixtures/metadata-editor.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/metadata-editor.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/metadata-list-entry.underscore b/cms/static/coffee/fixtures/metadata-list-entry.underscore
deleted file mode 120000
index 78fa4e2000..0000000000
--- a/cms/static/coffee/fixtures/metadata-list-entry.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/metadata-list-entry.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/metadata-number-entry.underscore b/cms/static/coffee/fixtures/metadata-number-entry.underscore
deleted file mode 120000
index 99138aa9c1..0000000000
--- a/cms/static/coffee/fixtures/metadata-number-entry.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/metadata-number-entry.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/metadata-option-entry.underscore b/cms/static/coffee/fixtures/metadata-option-entry.underscore
deleted file mode 120000
index c6cd499801..0000000000
--- a/cms/static/coffee/fixtures/metadata-option-entry.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/metadata-option-entry.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/metadata-string-entry.underscore b/cms/static/coffee/fixtures/metadata-string-entry.underscore
deleted file mode 120000
index f713ab5387..0000000000
--- a/cms/static/coffee/fixtures/metadata-string-entry.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/metadata-string-entry.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/no-textbooks.underscore b/cms/static/coffee/fixtures/no-textbooks.underscore
deleted file mode 120000
index d2e1c9a71a..0000000000
--- a/cms/static/coffee/fixtures/no-textbooks.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/no-textbooks.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/section-name-edit.underscore b/cms/static/coffee/fixtures/section-name-edit.underscore
deleted file mode 120000
index 89284ccf90..0000000000
--- a/cms/static/coffee/fixtures/section-name-edit.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/section-name-edit.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/show-textbook.underscore b/cms/static/coffee/fixtures/show-textbook.underscore
deleted file mode 120000
index d2ba37d689..0000000000
--- a/cms/static/coffee/fixtures/show-textbook.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/show-textbook.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/system-feedback.underscore b/cms/static/coffee/fixtures/system-feedback.underscore
deleted file mode 120000
index 10893f87c4..0000000000
--- a/cms/static/coffee/fixtures/system-feedback.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/system-feedback.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/fixtures/upload-dialog.underscore b/cms/static/coffee/fixtures/upload-dialog.underscore
deleted file mode 120000
index e5637e9a53..0000000000
--- a/cms/static/coffee/fixtures/upload-dialog.underscore
+++ /dev/null
@@ -1 +0,0 @@
-../../../templates/js/upload-dialog.underscore
\ No newline at end of file
diff --git a/cms/static/coffee/spec/helpers.coffee b/cms/static/coffee/spec/helpers.coffee
index 116983edf5..a03e2a0e56 100644
--- a/cms/static/coffee/spec/helpers.coffee
+++ b/cms/static/coffee/spec/helpers.coffee
@@ -1,4 +1,4 @@
-jasmine.getFixtures().fixturesPath = 'fixtures'
+jasmine.getFixtures().fixturesPath += 'coffee/fixtures'
# Stub jQuery.cookie
@stubCookies =
diff --git a/cms/static/coffee/spec/views/course_info_spec.coffee b/cms/static/coffee/spec/views/course_info_spec.coffee
new file mode 100644
index 0000000000..297e78f34a
--- /dev/null
+++ b/cms/static/coffee/spec/views/course_info_spec.coffee
@@ -0,0 +1,144 @@
+courseInfoPage = """
+
+
+
+
+
+
+
+
+ """
+
+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($("
+
+
%block>
diff --git a/cms/urls.py b/cms/urls.py
index 8f396d3742..0f5209173b 100644
--- a/cms/urls.py
+++ b/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')),
diff --git a/cms/wsgi.py b/cms/wsgi.py
new file mode 100644
index 0000000000..607d7ee709
--- /dev/null
+++ b/cms/wsgi.py
@@ -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()
+
diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py
index 2e519edb30..a17df56a71 100644
--- a/common/djangoapps/course_groups/tests/tests.py
+++ b/common/djangoapps/course_groups/tests/tests.py
@@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts,
is_commentable_cohorted, get_cohort_by_name)
-from xmodule.modulestore.django import modulestore, _MODULESTORES
+from xmodule.modulestore.django import modulestore, clear_existing_modulestores
-from xmodule.modulestore.tests.django_utils import xml_store_config
+from xmodule.modulestore.tests.django_utils import mixed_store_config
# NOTE: running this with the lms.envs.test config works without
# manually overriding the modulestore. However, running with
# cms.envs.test doesn't.
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
-TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
+TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'}
+TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING)
-@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
+@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestCohorts(django.test.TestCase):
@staticmethod
@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase):
"""
Make sure that course is reloaded every time--clear out the modulestore.
"""
- # don't like this, but don't know a better way to undo all changes made
- # to course. We don't have a course.clone() method.
- _MODULESTORES.clear()
+ clear_existing_modulestores()
def test_get_cohort(self):
"""
diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py
index eff944d450..d5f0ba7503 100644
--- a/common/djangoapps/course_modes/models.py
+++ b/common/djangoapps/course_modes/models.py
@@ -57,11 +57,6 @@ class CourseMode(models.Model):
def modes_for_course_dict(cls, course_id):
return { mode.slug : mode for mode in cls.modes_for_course(course_id) }
- def __unicode__(self):
- return u"{} : {}, min={}, prices={}".format(
- self.course_id, self.mode_slug, self.min_price, self.suggested_prices
- )
-
@classmethod
def mode_for_course(cls, course_id, mode_slug):
"""
@@ -76,3 +71,8 @@ class CourseMode(models.Model):
return matched[0]
else:
return None
+
+ def __unicode__(self):
+ return u"{} : {}, min={}, prices={}".format(
+ self.course_id, self.mode_slug, self.min_price, self.suggested_prices
+ )
diff --git a/common/djangoapps/datadog/startup.py b/common/djangoapps/datadog/startup.py
new file mode 100644
index 0000000000..41949c3a94
--- /dev/null
+++ b/common/djangoapps/datadog/startup.py
@@ -0,0 +1,12 @@
+from django.conf import settings
+from dogapi import dog_http_api, dog_stats_api
+
+def run():
+ """
+ Initialize connection to datadog during django startup.
+
+ Expects the datadog api key in the DATADOG_API settings key
+ """
+ if hasattr(settings, 'DATADOG_API'):
+ dog_http_api.api_key = settings.DATADOG_API
+ dog_stats_api.start(api_key=settings.DATADOG_API, statsd=True)
diff --git a/common/djangoapps/external_auth/tests/test_shib.py b/common/djangoapps/external_auth/tests/test_shib.py
index 6bb9c38e6f..0355730256 100644
--- a/common/djangoapps/external_auth/tests/test_shib.py
+++ b/common/djangoapps/external_auth/tests/test_shib.py
@@ -14,11 +14,9 @@ from django.contrib.auth.models import AnonymousUser, User
from django.utils.importlib import import_module
from xmodule.modulestore.tests.factories import CourseFactory
-from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
+from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
from xmodule.modulestore.inheritance import own_metadata
-from xmodule.modulestore.django import modulestore
-
-from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
+from xmodule.modulestore.django import editable_modulestore
from external_auth.models import ExternalAuthMap
from external_auth.views import shib_login, course_specific_login, course_specific_register
@@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment
from student.models import UserProfile, Registration, CourseEnrollment
from student.tests.factories import UserFactory
+TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
+
# Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider'
# attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present
# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
@@ -64,7 +64,7 @@ def gen_all_identities():
yield _build_identity_dict(mail, given_name, surname)
-@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
+@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
class ShibSPTest(ModuleStoreTestCase):
"""
Tests for the Shibboleth SP, which communicates via request.META
@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase):
request_factory = RequestFactory()
def setUp(self):
- self.store = modulestore()
+ self.store = editable_modulestore()
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
def test_exception_shib_login(self):
diff --git a/common/djangoapps/mitxmako/middleware.py b/common/djangoapps/mitxmako/middleware.py
index 5646d2f4b5..daaddf6b87 100644
--- a/common/djangoapps/mitxmako/middleware.py
+++ b/common/djangoapps/mitxmako/middleware.py
@@ -12,36 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from mako.lookup import TemplateLookup
-import tempdir
from django.template import RequestContext
-from django.conf import settings
requestcontext = None
-lookup = {}
-
class MakoMiddleware(object):
- def __init__(self):
- """Setup mako variables and lookup object"""
- # Set all mako variables based on django settings
- template_locations = settings.MAKO_TEMPLATES
- module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
-
- if module_directory is None:
- module_directory = tempdir.mkdtemp_clean()
-
- for location in template_locations:
- lookup[location] = TemplateLookup(directories=template_locations[location],
- module_directory=module_directory,
- output_encoding='utf-8',
- input_encoding='utf-8',
- default_filters=['decode.utf8'],
- encoding_errors='replace',
- )
-
- import mitxmako
- mitxmako.lookup = lookup
def process_request(self, request):
global requestcontext
diff --git a/common/djangoapps/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py
index 3c68fa85be..974eaefdd3 100644
--- a/common/djangoapps/mitxmako/shortcuts.py
+++ b/common/djangoapps/mitxmako/shortcuts.py
@@ -16,7 +16,7 @@ from django.template import Context
from django.http import HttpResponse
import logging
-from . import middleware
+import mitxmako
from django.conf import settings
from django.core.urlresolvers import reverse
log = logging.getLogger(__name__)
@@ -80,15 +80,15 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_instance['marketing_link'] = marketing_link
# In various testing contexts, there might not be a current request context.
- if middleware.requestcontext is not None:
- for d in middleware.requestcontext:
+ if mitxmako.middleware.requestcontext is not None:
+ for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
if context:
context_dictionary.update(context)
# fetch and render template
- template = middleware.lookup[namespace].get_template(template_name)
+ template = mitxmako.lookup[namespace].get_template(template_name)
return template.render_unicode(**context_dictionary)
diff --git a/common/djangoapps/mitxmako/startup.py b/common/djangoapps/mitxmako/startup.py
new file mode 100644
index 0000000000..db9483b366
--- /dev/null
+++ b/common/djangoapps/mitxmako/startup.py
@@ -0,0 +1,33 @@
+"""
+Initialize the mako template lookup
+"""
+
+import tempdir
+from django.conf import settings
+from mako.lookup import TemplateLookup
+
+import mitxmako
+
+
+def run():
+ """Setup mako variables and lookup object"""
+ # Set all mako variables based on django settings
+ template_locations = settings.MAKO_TEMPLATES
+ module_directory = getattr(settings, 'MAKO_MODULE_DIR', None)
+
+ if module_directory is None:
+ module_directory = tempdir.mkdtemp_clean()
+
+ lookup = {}
+
+ for location in template_locations:
+ lookup[location] = TemplateLookup(
+ directories=template_locations[location],
+ module_directory=module_directory,
+ output_encoding='utf-8',
+ input_encoding='utf-8',
+ default_filters=['decode.utf8'],
+ encoding_errors='replace',
+ )
+
+ mitxmako.lookup = lookup
diff --git a/common/djangoapps/mitxmako/template.py b/common/djangoapps/mitxmako/template.py
index 5becfbf1df..7dfc6de026 100644
--- a/common/djangoapps/mitxmako/template.py
+++ b/common/djangoapps/mitxmako/template.py
@@ -16,7 +16,8 @@ from django.conf import settings
from mako.template import Template as MakoTemplate
from mitxmako.shortcuts import marketing_link
-from mitxmako import middleware
+import mitxmako
+import mitxmako.middleware
django_variables = ['lookup', 'output_encoding', 'encoding_errors']
@@ -33,7 +34,7 @@ class Template(MakoTemplate):
def __init__(self, *args, **kwargs):
"""Overrides base __init__ to provide django variable overrides"""
if not kwargs.get('no_django', False):
- overrides = dict([(k, getattr(middleware, k, None),) for k in django_variables])
+ overrides = dict([(k, getattr(mitxmako, k, None),) for k in django_variables])
overrides['lookup'] = overrides['lookup']['main']
kwargs.update(overrides)
super(Template, self).__init__(*args, **kwargs)
@@ -47,8 +48,8 @@ class Template(MakoTemplate):
context_dictionary = {}
# In various testing contexts, there might not be a current request context.
- if middleware.requestcontext is not None:
- for d in middleware.requestcontext:
+ if mitxmako.middleware.requestcontext is not None:
+ for d in mitxmako.middleware.requestcontext:
context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
diff --git a/common/djangoapps/static_replace/test/test_static_replace.py b/common/djangoapps/static_replace/test/test_static_replace.py
index b1bc05b895..43a199c22c 100644
--- a/common/djangoapps/static_replace/test/test_static_replace.py
+++ b/common/djangoapps/static_replace/test/test_static_replace.py
@@ -1,6 +1,6 @@
import re
-from nose.tools import assert_equals, assert_true, assert_false
+from nose.tools import assert_equals, assert_true, assert_false # pylint: disable=E0611
from static_replace import (replace_static_urls, replace_course_urls,
_url_replace_regex)
from mock import patch, Mock
diff --git a/common/djangoapps/student/management/commands/6002exportusers.py b/common/djangoapps/student/management/commands/6002exportusers.py
index a92bb0a60c..a36d6e84c6 100644
--- a/common/djangoapps/student/management/commands/6002exportusers.py
+++ b/common/djangoapps/student/management/commands/6002exportusers.py
@@ -16,10 +16,6 @@ from django.contrib.auth.models import User
from student.models import UserProfile
-import mitxmako.middleware as middleware
-
-middleware.MakoMiddleware()
-
class Command(BaseCommand):
help = \
diff --git a/common/djangoapps/student/management/commands/6002importusers.py b/common/djangoapps/student/management/commands/6002importusers.py
index 1f98bd7522..93d86dfc05 100644
--- a/common/djangoapps/student/management/commands/6002importusers.py
+++ b/common/djangoapps/student/management/commands/6002importusers.py
@@ -12,10 +12,6 @@ from django.contrib.auth.models import User
from student.models import UserProfile
-import mitxmako.middleware as middleware
-
-middleware.MakoMiddleware()
-
def import_user(u):
user_info = u['u']
diff --git a/common/djangoapps/student/management/commands/assigngroups.py b/common/djangoapps/student/management/commands/assigngroups.py
index 3e36bf3129..cbd5cfad22 100644
--- a/common/djangoapps/student/management/commands/assigngroups.py
+++ b/common/djangoapps/student/management/commands/assigngroups.py
@@ -1,7 +1,6 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
-import mitxmako.middleware as middleware
from student.models import UserTestGroup
import random
@@ -11,8 +10,6 @@ import datetime
import json
from pytz import UTC
-middleware.MakoMiddleware()
-
def group_from_value(groups, v):
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
diff --git a/common/djangoapps/student/management/commands/emaillist.py b/common/djangoapps/student/management/commands/emaillist.py
index d3911927ac..e69b072db2 100644
--- a/common/djangoapps/student/management/commands/emaillist.py
+++ b/common/djangoapps/student/management/commands/emaillist.py
@@ -1,10 +1,6 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
-import mitxmako.middleware as middleware
-
-middleware.MakoMiddleware()
-
class Command(BaseCommand):
help = \
diff --git a/common/djangoapps/student/management/commands/massemail.py b/common/djangoapps/student/management/commands/massemail.py
index 1bb65fd169..a1864f048e 100644
--- a/common/djangoapps/student/management/commands/massemail.py
+++ b/common/djangoapps/student/management/commands/massemail.py
@@ -1,9 +1,7 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
-import mitxmako.middleware as middleware
-
-middleware.MakoMiddleware()
+import mitxmako
class Command(BaseCommand):
@@ -17,8 +15,8 @@ body, and an _subject.txt for the subject. '''
#text = open(args[0]).read()
#subject = open(args[1]).read()
users = User.objects.all()
- text = middleware.lookup['main'].get_template('email/' + args[0] + ".txt").render()
- subject = middleware.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
+ text = mitxmako.lookup['main'].get_template('email/' + args[0] + ".txt").render()
+ subject = mitxmako.lookup['main'].get_template('email/' + args[0] + "_subject.txt").render().strip()
for user in users:
if user.is_active:
user.email_user(subject, text)
diff --git a/common/djangoapps/student/management/commands/massemailtxt.py b/common/djangoapps/student/management/commands/massemailtxt.py
index ae25430a85..0228acf923 100644
--- a/common/djangoapps/student/management/commands/massemailtxt.py
+++ b/common/djangoapps/student/management/commands/massemailtxt.py
@@ -4,15 +4,13 @@ import time
from django.core.management.base import BaseCommand
from django.conf import settings
-import mitxmako.middleware as middleware
+import mitxmako
from django.core.mail import send_mass_mail
import sys
import datetime
-middleware.MakoMiddleware()
-
def chunks(l, n):
""" Yield successive n-sized chunks from l.
@@ -41,8 +39,8 @@ rate -- messages per second
users = [u.strip() for u in open(user_file).readlines()]
- message = middleware.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
- subject = middleware.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
+ message = mitxmako.lookup['main'].get_template('emails/' + message_base + "_body.txt").render()
+ subject = mitxmako.lookup['main'].get_template('emails/' + message_base + "_subject.txt").render().strip()
rate = int(ratestr)
self.log_file = open(logfilename, "a+", buffering=0)
diff --git a/common/djangoapps/student/management/commands/userinfo.py b/common/djangoapps/student/management/commands/userinfo.py
index 5467db1733..8656fb9183 100644
--- a/common/djangoapps/student/management/commands/userinfo.py
+++ b/common/djangoapps/student/management/commands/userinfo.py
@@ -1,13 +1,10 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
-import mitxmako.middleware as middleware
import json
from student.models import UserProfile
-middleware.MakoMiddleware()
-
class Command(BaseCommand):
help = \
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 3d977b28c9..5f29ffa6aa 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -805,7 +805,8 @@ class CourseEnrollment(models.Model):
record.is_active = False
record.save()
except cls.DoesNotExist:
- log.error("Tried to unenroll student {} from {} but they were not enrolled")
+ err_msg = u"Tried to unenroll student {} from {} but they were not enrolled"
+ log.error(err_msg.format(user, course_id))
@classmethod
def unenroll_by_email(cls, email, course_id):
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 9173eb8224..7b02cc37d5 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1,3 +1,6 @@
+"""
+Student Views
+"""
import datetime
import json
import logging
@@ -52,6 +55,10 @@ from courseware.access import has_access
from external_auth.models import ExternalAuthMap
+from bulk_email.models import Optout
+
+import track.views
+
from statsd import statsd
from pytz import UTC
@@ -62,8 +69,7 @@ Article = namedtuple('Article', 'title url author image deck publication publish
def csrf_token(context):
- ''' A csrf token that can be included in a form.
- '''
+ """A csrf token that can be included in a form."""
csrf_token = context.get('csrf_token', '')
if csrf_token == 'NOTPROVIDED':
return ''
@@ -76,12 +82,12 @@ def csrf_token(context):
# This means that it should always return the same thing for anon
# users. (in particular, no switching based on query params allowed)
def index(request, extra_context={}, user=None):
- '''
+ """
Render the edX main page.
extra_context is used to allow immediate display of certain modal windows, eg signup,
as used by external_auth.
- '''
+ """
# The course selection work is done in courseware.courses.
domain = settings.MITX_FEATURES.get('FORCE_UNIVERSITY_DOMAIN') # normally False
@@ -265,6 +271,8 @@ def dashboard(request):
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
+ course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
+
message = ""
if not user.is_active:
message = render_to_string('registration/activate_account_notice.html', {'email': user.email})
@@ -292,6 +300,7 @@ def dashboard(request):
pass
context = {'courses': courses,
+ 'course_optouts': course_optouts,
'message': message,
'external_auth_map': external_auth_map,
'staff_access': staff_access,
@@ -411,7 +420,7 @@ def accounts_login(request, error=""):
# Need different levels of logging
@ensure_csrf_cookie
def login_user(request, error=""):
- ''' AJAX request to log in the user. '''
+ """AJAX request to log in the user."""
if 'email' not in request.POST or 'password' not in request.POST:
return HttpResponse(json.dumps({'success': False,
'value': _('There was an error receiving your login information. Please email us.')})) # TODO: User error message
@@ -494,11 +503,11 @@ def login_user(request, error=""):
@ensure_csrf_cookie
def logout_user(request):
- '''
+ """
HTTP request to log out the user. Redirects to marketing page.
Deletes both the CSRF and sessionid cookies so the marketing
site can determine the logged in state of the user
- '''
+ """
# We do not log here, because we have a handler registered
# to perform logging on successful logouts.
logout(request)
@@ -512,8 +521,7 @@ def logout_user(request):
@login_required
@ensure_csrf_cookie
def change_setting(request):
- ''' JSON call to change a profile setting: Right now, location
- '''
+ """JSON call to change a profile setting: Right now, location"""
# TODO (vshnayder): location is no longer used
up = UserProfile.objects.get(user=request.user) # request.user.profile_cache
if 'location' in request.POST:
@@ -581,10 +589,10 @@ def _do_create_account(post_vars):
@ensure_csrf_cookie
def create_account(request, post_override=None):
- '''
+ """
JSON call to create new edX account.
Used by form in signup_modal.html, which is included into navigation.html
- '''
+ """
js = {'success': False}
post_vars = post_override if post_override else request.POST
@@ -818,10 +826,10 @@ def begin_exam_registration(request, course_id):
@ensure_csrf_cookie
def create_exam_registration(request, post_override=None):
- '''
+ """
JSON call to create a test center exam registration.
Called by form in test_center_register.html
- '''
+ """
post_vars = post_override if post_override else request.POST
# first determine if we need to create a new TestCenterUser, or if we are making any update
@@ -974,8 +982,7 @@ def auto_auth(request):
@ensure_csrf_cookie
def activate_account(request, key):
- ''' When link in activation e-mail is clicked
- '''
+ """When link in activation e-mail is clicked"""
r = Registration.objects.filter(activation_key=key)
if len(r) == 1:
user_logged_in = request.user.is_authenticated()
@@ -1010,7 +1017,7 @@ def activate_account(request, key):
@ensure_csrf_cookie
def password_reset(request):
- ''' Attempts to send a password reset e-mail. '''
+ """ Attempts to send a password reset e-mail. """
if request.method != "POST":
raise Http404
@@ -1032,9 +1039,9 @@ def password_reset_confirm_wrapper(
uidb36=None,
token=None,
):
- ''' A wrapper around django.contrib.auth.views.password_reset_confirm.
+ """ A wrapper around django.contrib.auth.views.password_reset_confirm.
Needed because we want to set the user as active at this step.
- '''
+ """
# cribbed from django.contrib.auth.views.password_reset_confirm
try:
uid_int = base36_to_int(uidb36)
@@ -1076,8 +1083,8 @@ def reactivation_email_for_user(user):
@ensure_csrf_cookie
def change_email_request(request):
- ''' AJAX call from the profile page. User wants a new e-mail.
- '''
+ """ AJAX call from the profile page. User wants a new e-mail.
+ """
## Make sure it checks for existing e-mail conflicts
if not request.user.is_authenticated:
raise Http404
@@ -1132,9 +1139,9 @@ def change_email_request(request):
@ensure_csrf_cookie
@transaction.commit_manually
def confirm_email_change(request, key):
- ''' User requested a new e-mail. This is called when the activation
+ """ User requested a new e-mail. This is called when the activation
link is clicked. We confirm with the old e-mail, and update
- '''
+ """
try:
try:
pec = PendingEmailChange.objects.get(activation_key=key)
@@ -1191,7 +1198,7 @@ def confirm_email_change(request, key):
@ensure_csrf_cookie
def change_name_request(request):
- ''' Log a request for a new name. '''
+ """ Log a request for a new name. """
if not request.user.is_authenticated:
raise Http404
@@ -1215,7 +1222,7 @@ def change_name_request(request):
@ensure_csrf_cookie
def pending_name_changes(request):
- ''' Web page which allows staff to approve or reject name changes. '''
+ """ Web page which allows staff to approve or reject name changes. """
if not request.user.is_staff:
raise Http404
@@ -1231,7 +1238,7 @@ def pending_name_changes(request):
@ensure_csrf_cookie
def reject_name_change(request):
- ''' JSON: Name change process. Course staff clicks 'reject' on a given name change '''
+ """ JSON: Name change process. Course staff clicks 'reject' on a given name change """
if not request.user.is_staff:
raise Http404
@@ -1269,13 +1276,36 @@ def accept_name_change_by_id(id):
@ensure_csrf_cookie
def accept_name_change(request):
- ''' JSON: Name change process. Course staff clicks 'accept' on a given name change
+ """ JSON: Name change process. Course staff clicks 'accept' on a given name change
We used this during the prototype but now we simply record name changes instead
of manually approving them. Still keeping this around in case we want to go
back to this approval method.
- '''
+ """
if not request.user.is_staff:
raise Http404
return accept_name_change_by_id(int(request.POST['id']))
+
+
+@require_POST
+@login_required
+@ensure_csrf_cookie
+def change_email_settings(request):
+ """Modify logged-in user's setting for receiving emails from a course."""
+ user = request.user
+
+ course_id = request.POST.get("course_id")
+ receive_emails = request.POST.get("receive_emails")
+ if receive_emails:
+ optout_object = Optout.objects.filter(user=user, course_id=course_id)
+ if optout_object:
+ optout_object.delete()
+ log.info(u"User {0} ({1}) opted in to receive emails from course {2}".format(user.username, user.email, course_id))
+ track.views.server_track(request, "change-email-settings", {"receive_emails": "yes", "course": course_id}, page='dashboard')
+ else:
+ Optout.objects.get_or_create(user=user, course_id=course_id)
+ log.info(u"User {0} ({1}) opted out of receiving emails from course {2}".format(user.username, user.email, course_id))
+ track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
+
+ return HttpResponse(json.dumps({'success': True}))
diff --git a/common/djangoapps/terrain/browser.py b/common/djangoapps/terrain/browser.py
index cf53aa4f69..75c0764b1b 100644
--- a/common/djangoapps/terrain/browser.py
+++ b/common/djangoapps/terrain/browser.py
@@ -16,11 +16,6 @@ from requests import put
from base64 import encodestring
from json import dumps
-# Let the LMS and CMS do their one-time setup
-# For example, setting up mongo caches
-# These names aren't used, but do important work on import.
-from lms import one_time_startup # pylint: disable=W0611
-from cms import one_time_startup # pylint: disable=W0611
from pymongo import MongoClient
import xmodule.modulestore.django
from xmodule.contentstore.django import _CONTENTSTORE
@@ -161,9 +156,10 @@ def reset_databases(scenario):
mongo = MongoClient()
mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear()
- modulestore = xmodule.modulestore.django.modulestore()
+
+ modulestore = xmodule.modulestore.django.editable_modulestore()
modulestore.collection.drop()
- xmodule.modulestore.django._MODULESTORES.clear()
+ xmodule.modulestore.django.clear_existing_modulestores()
# Uncomment below to trigger a screenshot on error
diff --git a/common/djangoapps/terrain/course_helpers.py b/common/djangoapps/terrain/course_helpers.py
index eca3290080..fc01d25d66 100644
--- a/common/djangoapps/terrain/course_helpers.py
+++ b/common/djangoapps/terrain/course_helpers.py
@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login
from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment
-from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.django import editable_modulestore
from xmodule.contentstore.django import contentstore
from urllib import quote_plus
@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False):
@world.absorb
def clear_courses():
# Flush and initialize the module store
- # It needs the templates because it creates new records
- # by cloning from the template.
# Note that if your test module gets in some weird state
# (though it shouldn't), do this manually
# from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()"
- modulestore().collection.drop()
+ editable_modulestore().collection.drop()
contentstore().fs_files.drop()
diff --git a/common/djangoapps/terrain/steps.py b/common/djangoapps/terrain/steps.py
index f13b3ff932..c4783d4aca 100644
--- a/common/djangoapps/terrain/steps.py
+++ b/common/djangoapps/terrain/steps.py
@@ -15,7 +15,7 @@ from lettuce import world, step
from .course_helpers import *
from .ui_helpers import *
from lettuce.django import django_url
-from nose.tools import assert_equals
+from nose.tools import assert_equals # pylint: disable=E0611
from logging import getLogger
logger = getLogger(__name__)
diff --git a/common/djangoapps/terrain/ui_helpers.py b/common/djangoapps/terrain/ui_helpers.py
index 3ab7e11b47..7d308931b2 100644
--- a/common/djangoapps/terrain/ui_helpers.py
+++ b/common/djangoapps/terrain/ui_helpers.py
@@ -10,7 +10,7 @@ from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from lettuce.django import django_url
-from nose.tools import assert_true
+from nose.tools import assert_true # pylint: disable=E0611
@world.absorb
diff --git a/common/lib/calc/calc/__init__.py b/common/lib/calc/calc/__init__.py
new file mode 100644
index 0000000000..e0d80d7b89
--- /dev/null
+++ b/common/lib/calc/calc/__init__.py
@@ -0,0 +1,6 @@
+"""
+Ideally, we wouldn't need to pull in all the calc symbols here,
+but courses were using 'import calc', so we need this for
+backwards compatibility
+"""
+from calc import *
diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc/calc.py
similarity index 96%
rename from common/lib/calc/calc.py
rename to common/lib/calc/calc/calc.py
index ab300f121b..efb30f93d7 100644
--- a/common/lib/calc/calc.py
+++ b/common/lib/calc/calc/calc.py
@@ -9,7 +9,7 @@ import operator
import numbers
import numpy
import scipy.constants
-import calcfunctions
+import functions
from pyparsing import (
Word, Literal, CaselessLiteral, ZeroOrMore, MatchFirst, Optional, Forward,
@@ -20,9 +20,9 @@ DEFAULT_FUNCTIONS = {
'sin': numpy.sin,
'cos': numpy.cos,
'tan': numpy.tan,
- 'sec': calcfunctions.sec,
- 'csc': calcfunctions.csc,
- 'cot': calcfunctions.cot,
+ 'sec': functions.sec,
+ 'csc': functions.csc,
+ 'cot': functions.cot,
'sqrt': numpy.sqrt,
'log10': numpy.log10,
'log2': numpy.log2,
@@ -31,24 +31,24 @@ DEFAULT_FUNCTIONS = {
'arccos': numpy.arccos,
'arcsin': numpy.arcsin,
'arctan': numpy.arctan,
- 'arcsec': calcfunctions.arcsec,
- 'arccsc': calcfunctions.arccsc,
- 'arccot': calcfunctions.arccot,
+ 'arcsec': functions.arcsec,
+ 'arccsc': functions.arccsc,
+ 'arccot': functions.arccot,
'abs': numpy.abs,
'fact': math.factorial,
'factorial': math.factorial,
'sinh': numpy.sinh,
'cosh': numpy.cosh,
'tanh': numpy.tanh,
- 'sech': calcfunctions.sech,
- 'csch': calcfunctions.csch,
- 'coth': calcfunctions.coth,
+ 'sech': functions.sech,
+ 'csch': functions.csch,
+ 'coth': functions.coth,
'arcsinh': numpy.arcsinh,
'arccosh': numpy.arccosh,
'arctanh': numpy.arctanh,
- 'arcsech': calcfunctions.arcsech,
- 'arccsch': calcfunctions.arccsch,
- 'arccoth': calcfunctions.arccoth
+ 'arcsech': functions.arcsech,
+ 'arccsch': functions.arccsch,
+ 'arccoth': functions.arccoth
}
DEFAULT_VARIABLES = {
'i': numpy.complex(0, 1),
diff --git a/common/lib/calc/calcfunctions.py b/common/lib/calc/calc/functions.py
similarity index 100%
rename from common/lib/calc/calcfunctions.py
rename to common/lib/calc/calc/functions.py
diff --git a/common/lib/calc/preview.py b/common/lib/calc/calc/preview.py
similarity index 100%
rename from common/lib/calc/preview.py
rename to common/lib/calc/calc/preview.py
diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/calc/tests/test_calc.py
similarity index 100%
rename from common/lib/calc/tests/test_calc.py
rename to common/lib/calc/calc/tests/test_calc.py
diff --git a/common/lib/calc/tests/test_preview.py b/common/lib/calc/calc/tests/test_preview.py
similarity index 99%
rename from common/lib/calc/tests/test_preview.py
rename to common/lib/calc/calc/tests/test_preview.py
index 0008cdda47..7db307a742 100644
--- a/common/lib/calc/tests/test_preview.py
+++ b/common/lib/calc/calc/tests/test_preview.py
@@ -4,7 +4,7 @@ Unit tests for preview.py
"""
import unittest
-import preview
+from calc import preview
import pyparsing
diff --git a/common/lib/calc/setup.py b/common/lib/calc/setup.py
index cb638914f9..361884babf 100644
--- a/common/lib/calc/setup.py
+++ b/common/lib/calc/setup.py
@@ -2,8 +2,8 @@ from setuptools import setup
setup(
name="calc",
- version="0.1.1",
- py_modules=["calc"],
+ version="0.2",
+ packages=["calc"],
install_requires=[
"pyparsing==1.5.6",
"numpy",
diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py
index c2bdeadc21..08a223f609 100644
--- a/common/lib/capa/capa/capa_problem.py
+++ b/common/lib/capa/capa/capa_problem.py
@@ -555,6 +555,13 @@ class LoncapaProblem(object):
Used by get_html.
'''
+ if not isinstance(problemtree.tag, basestring):
+ # Comment and ProcessingInstruction nodes are not Elements,
+ # and we're ok leaving those behind.
+ # BTW: etree gives us no good way to distinguish these things
+ # other than to examine .tag to see if it's a string. :(
+ return
+
if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
# leave javascript intact.
diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py
index 9defd2c5e6..d27893d44d 100644
--- a/common/lib/capa/capa/inputtypes.py
+++ b/common/lib/capa/capa/inputtypes.py
@@ -49,7 +49,7 @@ import pyparsing
from .registry import TagRegistry
from chem import chemcalc
-from preview import latex_preview
+from calc.preview import latex_preview
import xqueue_interface
from datetime import datetime
diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py
index 731230ecc1..b53f38fd90 100644
--- a/common/lib/capa/capa/responsetypes.py
+++ b/common/lib/capa/capa/responsetypes.py
@@ -915,7 +915,26 @@ class NumericalResponse(LoncapaResponse):
else:
return CorrectMap(self.answer_id, 'incorrect')
- # TODO: add check_hint_condition(self, hxml_set, student_answers)
+ def compare_answer(self, ans1, ans2):
+ """
+ Outside-facing function that lets us compare two numerical answers,
+ with this problem's tolerance.
+ """
+ return compare_with_tolerance(
+ evaluator({}, {}, ans1),
+ evaluator({}, {}, ans2),
+ self.tolerance
+ )
+
+ def validate_answer(self, answer):
+ """
+ Returns whether this answer is in a valid form.
+ """
+ try:
+ evaluator(dict(), dict(), answer)
+ return True
+ except (StudentInputError, UndefinedVariable):
+ return False
def get_answers(self):
return {self.answer_id: self.correct_answer}
@@ -1778,46 +1797,24 @@ class FormulaResponse(LoncapaResponse):
self.correct_answer, given, self.samples)
return CorrectMap(self.answer_id, correctness)
- def check_formula(self, expected, given, samples):
- variables = samples.split('@')[0].split(',')
- numsamples = int(samples.split('@')[1].split('#')[1])
- sranges = zip(*map(lambda x: map(float, x.split(",")),
- samples.split('@')[1].split('#')[0].split(':')))
-
- ranges = dict(zip(variables, sranges))
- for _ in range(numsamples):
- instructor_variables = self.strip_dict(dict(self.context))
- student_variables = {}
- # ranges give numerical ranges for testing
- for var in ranges:
- # TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
- value = random.uniform(*ranges[var])
- instructor_variables[str(var)] = value
- student_variables[str(var)] = value
- # log.debug('formula: instructor_vars=%s, expected=%s' %
- # (instructor_variables,expected))
-
- # Call `evaluator` on the instructor's answer and get a number
- instructor_result = evaluator(
- instructor_variables, {},
- expected, case_sensitive=self.case_sensitive
- )
+ def tupleize_answers(self, answer, var_dict_list):
+ """
+ Takes in an answer and a list of dictionaries mapping variables to values.
+ Each dictionary represents a test case for the answer.
+ Returns a tuple of formula evaluation results.
+ """
+ out = []
+ for var_dict in var_dict_list:
try:
- # log.debug('formula: student_vars=%s, given=%s' %
- # (student_variables,given))
-
- # Call `evaluator` on the student's answer; look for exceptions
- student_result = evaluator(
- student_variables,
- {},
- given,
- case_sensitive=self.case_sensitive
- )
+ out.append(evaluator(
+ var_dict,
+ dict(),
+ answer,
+ case_sensitive=self.case_sensitive,
+ ))
except UndefinedVariable as uv:
log.debug(
- 'formularesponse: undefined variable in given=%s',
- given
- )
+ 'formularesponse: undefined variable in formula=%s' % answer)
raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer"
)
@@ -1840,17 +1837,70 @@ class FormulaResponse(LoncapaResponse):
# If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve))
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
- cgi.escape(given))
+ cgi.escape(answer))
except Exception as err:
# traceback.print_exc()
log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
- cgi.escape(given))
+ cgi.escape(answer))
+ return out
- # No errors in student's response--actually test for correctness
- if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
- return "incorrect"
- return "correct"
+ def randomize_variables(self, samples):
+ """
+ Returns a list of dictionaries mapping variables to random values in range,
+ as expected by tupleize_answers.
+ """
+ variables = samples.split('@')[0].split(',')
+ numsamples = int(samples.split('@')[1].split('#')[1])
+ sranges = zip(*map(lambda x: map(float, x.split(",")),
+ samples.split('@')[1].split('#')[0].split(':')))
+ ranges = dict(zip(variables, sranges))
+
+ out = []
+ for i in range(numsamples):
+ var_dict = {}
+ # ranges give numerical ranges for testing
+ for var in ranges:
+ # TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
+ value = random.uniform(*ranges[var])
+ var_dict[str(var)] = value
+ out.append(var_dict)
+ return out
+
+ def check_formula(self, expected, given, samples):
+ """
+ Given an expected answer string, a given (student-produced) answer
+ string, and a samples string, return whether the given answer is
+ "correct" or "incorrect".
+ """
+ var_dict_list = self.randomize_variables(samples)
+ student_result = self.tupleize_answers(given, var_dict_list)
+ instructor_result = self.tupleize_answers(expected, var_dict_list)
+
+ correct = all(compare_with_tolerance(student, instructor, self.tolerance)
+ for student, instructor in zip(student_result, instructor_result))
+ if correct:
+ return "correct"
+ else:
+ return "incorrect"
+
+ def compare_answer(self, ans1, ans2):
+ """
+ An external interface for comparing whether a and b are equal.
+ """
+ internal_result = self.check_formula(ans1, ans2, self.samples)
+ return internal_result == "correct"
+
+ def validate_answer(self, answer):
+ """
+ Returns whether this answer is in a valid form.
+ """
+ var_dict_list = self.randomize_variables(self.samples)
+ try:
+ self.tupleize_answers(answer, var_dict_list)
+ return True
+ except StudentInputError:
+ return False
def strip_dict(self, d):
''' Takes a dict. Returns an identical dict, with all non-word
diff --git a/common/lib/capa/capa/safe_exec/README.rst b/common/lib/capa/capa/safe_exec/README.rst
index c61100f709..00b81ca15f 100644
--- a/common/lib/capa/capa/safe_exec/README.rst
+++ b/common/lib/capa/capa/safe_exec/README.rst
@@ -16,11 +16,11 @@ __ https://github.com/edx/codejail/blob/master/README.rst
1. At the instruction to install packages into the sandboxed code, you'll
- need to install both `pre-sandbox-requirements.txt` and
- `sandbox-requirements.txt`::
+ need to install the requirements from requirements/edx-sandbox::
- $ sudo pip install -r pre-sandbox-requirements.txt
- $ sudo pip install -r sandbox-requirements.txt
+ $ pip install -r requirements/edx-sandbox/base.txt
+ $ pip install -r requirements/edx-sandbox/local.txt
+ $ pip install -r requirements/edx-sandbox/post.txt
2. At the instruction to create the AppArmor profile, you'll need a line in
the profile for the sandbox packages. is the full path to
diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html
index 17f7efcec4..cc153efe3c 100644
--- a/common/lib/capa/capa/templates/choicegroup.html
+++ b/common/lib/capa/capa/templates/choicegroup.html
@@ -17,6 +17,7 @@
% for choice_id, choice_description in choices:
$', '', re.sub(r'^
', '', clean_html))
- except:
+ clean_html = re.sub("\n"," ", clean_html)
+ except Exception:
clean_html = answer
return clean_html
@@ -230,7 +231,7 @@ class OpenEndedChild(object):
'max_score': self._max_score,
'child_attempts': self.child_attempts,
'child_created': False,
- }
+ }
return json.dumps(state)
def _allow_reset(self):
@@ -332,7 +333,7 @@ class OpenEndedChild(object):
try:
image_data.seek(0)
image_ok = open_ended_image_submission.run_image_tests(image_data)
- except:
+ except Exception:
log.exception("Could not create image and check it.")
if image_ok:
@@ -345,7 +346,7 @@ class OpenEndedChild(object):
success, s3_public_url = open_ended_image_submission.upload_to_s3(
image_data, image_key, self.s3_interface
)
- except:
+ except Exception:
log.exception("Could not upload image to S3.")
return success, image_ok, s3_public_url
@@ -434,38 +435,6 @@ class OpenEndedChild(object):
return success, string
- def check_if_student_can_submit(self):
- location = self.location_string
-
- student_id = self.system.anonymous_student_id
- success = False
- allowed_to_submit = True
- response = {}
- # This is a student_facing_error
- error_string = ("You need to peer grade {0} more in order to make another submission. "
- "You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.")
- try:
- response = self.peer_gs.get_data_for_location(self.location_string, student_id)
- count_graded = response['count_graded']
- count_required = response['count_required']
- student_sub_count = response['student_sub_count']
- success = True
- except:
- # This is a dev_facing_error
- log.error("Could not contact external open ended graders for location {0} and student {1}".format(
- self.location_string, student_id))
- # This is a student_facing_error
- error_message = "Could not contact the graders. Please notify course staff."
- return success, allowed_to_submit, error_message
- if count_graded >= count_required:
- return success, allowed_to_submit, ""
- else:
- allowed_to_submit = False
- # This is a student_facing_error
- error_message = error_string.format(count_required - count_graded, count_graded, count_required,
- student_sub_count)
- return success, allowed_to_submit, error_message
-
def get_eta(self):
if self.controller_qs:
response = self.controller_qs.check_for_eta(self.location_string)
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
index 3c25b301ab..0e5c9cdda1 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/peer_grading_service.py
@@ -124,4 +124,4 @@ class MockPeerGradingService(object):
]}
def get_data_for_location(self, problem_location, student_id):
- return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1}
+ return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1, 'submissions_available' : 0}
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
index 1262e1f68f..2485fc77ea 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/self_assessment_module.py
@@ -61,6 +61,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
else:
previous_answer = ''
+ previous_answer = previous_answer.replace("\n"," ")
context = {
'prompt': self.child_prompt,
'previous_answer': previous_answer,
@@ -184,14 +185,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
# add new history element with answer and empty score and hint.
success, data = self.append_image_to_student_answer(data)
if success:
- success, allowed_to_submit, error_message = self.check_if_student_can_submit()
- if allowed_to_submit:
- data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer'])
- self.new_history_entry(data['student_answer'])
- self.change_state(self.ASSESSING)
- else:
- # Error message already defined
- success = False
+ data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer'])
+ self.new_history_entry(data['student_answer'])
+ self.change_state(self.ASSESSING)
else:
# This is a student_facing_error
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
@@ -200,7 +196,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'success': success,
'rubric_html': self.get_rubric_html(system),
'error': error_message,
- 'student_response': data['student_answer'],
+ 'student_response': data['student_answer'].replace("\n"," ")
}
def save_assessment(self, data, _system):
@@ -272,8 +268,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
try:
rubric_scores = json.loads(latest_post_assessment)
except:
- # This is a dev_facing_error
- log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment))
rubric_scores = []
return [rubric_scores]
diff --git a/common/lib/xmodule/xmodule/peer_grading_module.py b/common/lib/xmodule/xmodule/peer_grading_module.py
index 1ef3883d82..ae4a8b87de 100644
--- a/common/lib/xmodule/xmodule/peer_grading_module.py
+++ b/common/lib/xmodule/xmodule/peer_grading_module.py
@@ -8,10 +8,9 @@ from pkg_resources import resource_string
from .capa_module import ComplexEncoder
from .x_module import XModule
from xmodule.raw_module import RawDescriptor
-from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
from .timeinfo import TimeInfo
-from xblock.core import Dict, String, Scope, Boolean, Integer, Float
+from xblock.core import Dict, String, Scope, Boolean, Float
from xmodule.fields import Date, Timedelta
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
@@ -46,7 +45,6 @@ class PeerGradingFields(object):
)
due = Date(
help="Due date that should be displayed.",
- default=None,
scope=Scope.settings)
graceperiod = Timedelta(
help="Amount of grace to give on the due date.",
@@ -105,7 +103,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
if self.use_for_single_location:
try:
- self.linked_problem = modulestore().get_instance(self.system.course_id, self.link_to_location)
+ self.linked_problem = self.system.get_module(self.link_to_location)
except ItemNotFoundError:
log.error("Linked location {0} for peer grading module {1} does not exist".format(
self.link_to_location, self.location))
@@ -189,9 +187,8 @@ class PeerGradingModule(PeerGradingFields, XModule):
return json.dumps(d, cls=ComplexEncoder)
- def query_data_for_location(self):
+ def query_data_for_location(self, location):
student_id = self.system.anonymous_student_id
- location = self.link_to_location
success = False
response = {}
@@ -229,7 +226,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
count_graded = self.student_data_for_location['count_graded']
count_required = self.student_data_for_location['count_required']
except:
- success, response = self.query_data_for_location()
+ success, response = self.query_data_for_location(self.location)
if not success:
log.exception(
"No instance data found and could not get data from controller for loc {0} student {1}".format(
@@ -312,17 +309,26 @@ class PeerGradingModule(PeerGradingFields, XModule):
error: if there was an error in the submission, this is the error message
"""
- required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged', 'answer_unknown'])
- success, message = self._check_required(data, required)
+ required = ['location', 'submission_id', 'submission_key', 'score', 'feedback', 'submission_flagged', 'answer_unknown']
+ if data.get("submission_flagged", False) in ["false", False, "False", "FALSE"]:
+ required.append("rubric_scores[]")
+ success, message = self._check_required(data, set(required))
if not success:
return self._err_response(message)
data_dict = {k:data.get(k) for k in required}
- data_dict['rubric_scores'] = data.getlist('rubric_scores[]')
+ if 'rubric_scores[]' in required:
+ data_dict['rubric_scores'] = data.getlist('rubric_scores[]')
data_dict['grader_id'] = self.system.anonymous_student_id
try:
response = self.peer_gs.save_grade(**data_dict)
+ success, location_data = self.query_data_for_location(data_dict['location'])
+ #Don't check for success above because the response = statement will raise the same Exception as the one
+ #that will cause success to be false.
+ response.update({'required_done' : False})
+ if 'count_graded' in location_data and 'count_required' in location_data and int(location_data['count_graded'])>=int(location_data['count_required']):
+ response['required_done'] = True
return response
except GradingServiceError:
# This is a dev_facing_error
@@ -502,7 +508,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
error_text = "Could not get list of problems to peer grade. Please notify course staff."
log.error(error_text)
success = False
- except:
+ except Exception:
log.exception("Could not contact peer grading service.")
success = False
@@ -513,20 +519,24 @@ class PeerGradingModule(PeerGradingFields, XModule):
'''
try:
return modulestore().get_instance(self.system.course_id, location)
- except:
+ except Exception:
# the linked problem doesn't exist
log.error("Problem {0} does not exist in this course".format(location))
raise
+ good_problem_list = []
for problem in problem_list:
problem_location = problem['location']
- descriptor = _find_corresponding_module_for_location(problem_location)
+ try:
+ descriptor = _find_corresponding_module_for_location(problem_location)
+ except Exception:
+ continue
if descriptor:
problem['due'] = descriptor.lms.due
grace_period = descriptor.lms.graceperiod
try:
problem_timeinfo = TimeInfo(problem['due'], grace_period)
- except:
+ except Exception:
log.error("Malformed due date or grace period string for location {0}".format(problem_location))
raise
if self._closed(problem_timeinfo):
@@ -537,13 +547,14 @@ class PeerGradingModule(PeerGradingFields, XModule):
# if we can't find the due date, assume that it doesn't have one
problem['due'] = None
problem['closed'] = False
+ good_problem_list.append(problem)
ajax_url = self.ajax_url
html = self.system.render_template('peer_grading/peer_grading.html', {
'course_id': self.system.course_id,
'ajax_url': ajax_url,
'success': success,
- 'problem_list': problem_list,
+ 'problem_list': good_problem_list,
'error_text': error_text,
# Checked above
'staff_access': False,
@@ -620,3 +631,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
non_editable_fields = super(PeerGradingDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([PeerGradingFields.due, PeerGradingFields.graceperiod])
return non_editable_fields
+
+ def get_required_module_descriptors(self):
+ """Returns a list of XModuleDescritpor instances upon which this module depends, but are
+ not children of this module"""
+ if self.use_for_single_location:
+ return [self.system.load_item(self.link_to_location)]
+ else:
+ return []
diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py
index c28378210b..34ba8f6c69 100644
--- a/common/lib/xmodule/xmodule/template_module.py
+++ b/common/lib/xmodule/xmodule/template_module.py
@@ -2,7 +2,6 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from lxml import etree
from mako.template import Template
-from xmodule.modulestore.django import modulestore
class CustomTagModule(XModule):
@@ -56,7 +55,7 @@ class CustomTagDescriptor(RawDescriptor):
# cdodge: look up the template as a module
template_loc = self.location.replace(category='custom_tag_template', name=template_name)
- template_module = modulestore().get_instance(system.course_id, template_loc)
+ template_module = system.load_item(template_loc)
template_module_data = template_module.data
template = Template(template_module_data)
return template.render(**params)
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 80c4e41e8f..92d30fac8c 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -322,7 +322,8 @@ class CapaModuleTest(unittest.TestCase):
# We have to set up Django settings in order to use QueryDict
from django.conf import settings
- settings.configure()
+ if not settings.configured:
+ settings.configure()
# Valid GET param dict
valid_get_dict = self._querydict_from_dict({'input_1': 'test',
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 035f88c046..8a32f7e822 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -22,7 +22,10 @@ from xmodule.open_ended_grading_classes.grading_service_module import GradingSer
from xmodule.combined_open_ended_module import CombinedOpenEndedModule
from xmodule.modulestore import Location
from xmodule.tests import get_test_system, test_util_open_ended
-from xmodule.tests.test_util_open_ended import MockQueryDict, DummyModulestore
+from xmodule.progress import Progress
+from xmodule.tests.test_util_open_ended import (MockQueryDict, DummyModulestore, TEST_STATE_SA_IN,
+ MOCK_INSTANCE_STATE, TEST_STATE_SA, TEST_STATE_AI, TEST_STATE_AI2, TEST_STATE_AI2_INVALID,
+ TEST_STATE_SINGLE, TEST_STATE_PE_SINGLE)
import capa.xqueue_interface as xqueue_interface
@@ -73,6 +76,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self):
self.test_system = get_test_system()
+ self.test_system.open_ended_grading_interface = None
self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
@@ -203,7 +207,7 @@ class OpenEndedModuleTest(unittest.TestCase):
def setUp(self):
self.test_system = get_test_system()
-
+ self.test_system.open_ended_grading_interface = None
self.test_system.location = self.location
self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
@@ -410,6 +414,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
descriptor = Mock(data=full_definition)
test_system = get_test_system()
+ test_system.open_ended_grading_interface = None
combinedoe_container = CombinedOpenEndedModule(
test_system,
descriptor,
@@ -421,8 +426,6 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
)
def setUp(self):
- # TODO: this constructor call is definitely wrong, but neither branch
- # of the merge matches the module constructor. Someone (Vik?) should fix this.
self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
self.location,
self.definition,
@@ -432,16 +435,25 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
instance_state=self.static_data)
def test_get_tag_name(self):
+ """
+ Test to see if the xml tag name is correct
+ """
name = self.combinedoe.get_tag_name("Tag")
self.assertEqual(name, "t")
def test_get_last_response(self):
+ """
+ See if we can parse the last response
+ """
response_dict = self.combinedoe.get_last_response(0)
self.assertEqual(response_dict['type'], "selfassessment")
self.assertEqual(response_dict['max_score'], self.max_score)
self.assertEqual(response_dict['state'], CombinedOpenEndedV1Module.INITIAL)
def test_update_task_states(self):
+ """
+ See if we can update the task states properly
+ """
changed = self.combinedoe.update_task_states()
self.assertFalse(changed)
@@ -452,6 +464,9 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertTrue(changed)
def test_get_max_score(self):
+ """
+ Try to get the max score of the problem
+ """
self.combinedoe.update_task_states()
self.combinedoe.state = "done"
self.combinedoe.is_scored = True
@@ -459,24 +474,62 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(max_score, 1)
def test_container_get_max_score(self):
+ """
+ See if we can get the max score from the actual xmodule
+ """
#The progress view requires that this function be exposed
max_score = self.combinedoe_container.max_score()
self.assertEqual(max_score, None)
+ def test_container_get_progress(self):
+ """
+ See if we can get the progress from the actual xmodule
+ """
+ progress = self.combinedoe_container.max_score()
+ self.assertEqual(progress, None)
+
+ def test_get_progress(self):
+ """
+ Test if we can get the correct progress from the combined open ended class
+ """
+ self.combinedoe.update_task_states()
+ self.combinedoe.state = "done"
+ self.combinedoe.is_scored = True
+ progress = self.combinedoe.get_progress()
+ self.assertIsInstance(progress, Progress)
+
+ # progress._a is the score of the xmodule, which is 0 right now.
+ self.assertEqual(progress._a, 0)
+
+ # progress._b is the max_score (which is 1), divided by the weight (which is 1).
+ self.assertEqual(progress._b, 1)
+
def test_container_weight(self):
+ """
+ Check the problem weight in the container
+ """
weight = self.combinedoe_container.weight
self.assertEqual(weight, 1)
def test_container_child_weight(self):
+ """
+ Test the class to see if it picks up the right weight
+ """
weight = self.combinedoe_container.child_module.weight
self.assertEqual(weight, 1)
def test_get_score(self):
+ """
+ See if scoring works
+ """
score_dict = self.combinedoe.get_score()
self.assertEqual(score_dict['score'], 0)
self.assertEqual(score_dict['total'], 1)
def test_alternate_orderings(self):
+ """
+ Try multiple ordering of definitions to see if the problem renders different steps correctly.
+ """
t1 = self.task_xml1
t2 = self.task_xml2
xml_to_test = [[t1], [t2], [t1, t1], [t1, t2], [t2, t2], [t2, t1], [t1, t2, t1]]
@@ -494,9 +547,28 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
changed = combinedoe.update_task_states()
self.assertFalse(changed)
+ combinedoe = CombinedOpenEndedV1Module(self.test_system,
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state={'task_states' : TEST_STATE_SA})
+
+ combinedoe = CombinedOpenEndedV1Module(self.test_system,
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state={'task_states' : TEST_STATE_SA_IN})
+
+
def test_get_score_realistic(self):
- instance_state = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require more detail . one piece of information that is omitted is the amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality3\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in each of four separate , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
- instance_state = json.loads(instance_state)
+ """
+ Try to parse the correct score from a json instance state
+ """
+ instance_state = json.loads(MOCK_INSTANCE_STATE)
rubric = """
@@ -524,6 +596,74 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
self.assertEqual(score_dict['score'], 15.0)
self.assertEqual(score_dict['total'], 15.0)
+ def generate_oe_module(self, task_state, task_number, task_xml):
+ """
+ Return a combined open ended module with the specified parameters
+ """
+ definition = {'prompt': etree.XML(self.prompt), 'rubric': etree.XML(self.rubric),
+ 'task_xml': task_xml}
+ descriptor = Mock(data=definition)
+ instance_state = {'task_states': task_state, 'graded': True}
+ if task_number is not None:
+ instance_state.update({'current_task_number' : task_number})
+ combinedoe = CombinedOpenEndedV1Module(self.test_system,
+ self.location,
+ definition,
+ descriptor,
+ static_data=self.static_data,
+ metadata=self.metadata,
+ instance_state=instance_state)
+ return combinedoe
+
+ def ai_state_reset(self, task_state, task_number=None):
+ """
+ See if state is properly reset
+ """
+ combinedoe = self.generate_oe_module(task_state, task_number, [self.task_xml2])
+ html = combinedoe.get_html()
+ self.assertIsInstance(html, basestring)
+
+ score = combinedoe.get_score()
+ if combinedoe.is_scored:
+ self.assertEqual(score['score'], 0)
+ else:
+ self.assertEqual(score['score'], None)
+
+ def ai_state_success(self, task_state, task_number=None, iscore=2, tasks=None):
+ """
+ See if state stays the same
+ """
+ if tasks is None:
+ tasks = [self.task_xml1, self.task_xml2]
+ combinedoe = self.generate_oe_module(task_state, task_number, tasks)
+ html = combinedoe.get_html()
+ self.assertIsInstance(html, basestring)
+ score = combinedoe.get_score()
+ self.assertEqual(int(score['score']), iscore)
+
+ def test_ai_state_reset(self):
+ self.ai_state_reset(TEST_STATE_AI)
+
+ def test_ai_state2_reset(self):
+ self.ai_state_reset(TEST_STATE_AI2)
+
+ def test_ai_invalid_state(self):
+ self.ai_state_reset(TEST_STATE_AI2_INVALID)
+
+ def test_ai_state_rest_task_number(self):
+ self.ai_state_reset(TEST_STATE_AI, task_number=2)
+ self.ai_state_reset(TEST_STATE_AI, task_number=5)
+ self.ai_state_reset(TEST_STATE_AI, task_number=1)
+ self.ai_state_reset(TEST_STATE_AI, task_number=0)
+
+ def test_ai_state_success(self):
+ self.ai_state_success(TEST_STATE_AI)
+
+ def test_state_single(self):
+ self.ai_state_success(TEST_STATE_SINGLE, iscore=12)
+
+ def test_state_pe_single(self):
+ self.ai_state_success(TEST_STATE_PE_SINGLE, iscore=0, tasks=[self.task_xml2])
class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
"""
@@ -536,6 +676,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
def setUp(self):
self.test_system = get_test_system()
+ self.test_system.open_ended_grading_interface = None
self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"])
)
@@ -569,9 +710,9 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer
- module.handle_ajax("save_answer", {"student_answer": self.answer})
- status = module.handle_ajax("get_status", {})
- self.assertTrue(isinstance(status, basestring))
+ html = module.handle_ajax("get_html", {})
+ module.handle_ajax("save_answer", {"student_answer": self.answer, "can_upload_files" : False, "student_file" : None})
+ html = module.handle_ajax("get_html", {})
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
@@ -579,8 +720,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
- status = module.handle_ajax("get_status", {})
- self.assertTrue(isinstance(status, basestring))
+ rubric = module.handle_ajax("get_combined_rubric", {})
#Move to the next step in the problem
module.handle_ajax("next_problem", {})
@@ -613,11 +753,9 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
- #from nose.tools import set_trace; set_trace()
module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
- module.handle_ajax("get_status", {})
#Move to the next step in the problem
try:
@@ -660,15 +798,11 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
#Get html and other data client will request
module.get_html()
- legend = module.handle_ajax("get_legend", {})
- self.assertTrue(isinstance(legend, basestring))
- module.handle_ajax("get_status", {})
module.handle_ajax("skip_post_assessment", {})
- self.assertTrue(isinstance(legend, basestring))
#Get all results
- module.handle_ajax("get_results", {})
+ module.handle_ajax("get_combined_rubric", {})
#reset the problem
module.handle_ajax("reset", {})
@@ -686,6 +820,7 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
def setUp(self):
self.test_system = get_test_system()
+ self.test_system.open_ended_grading_interface = None
self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"])
)
@@ -702,8 +837,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
#Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer": self.answer})
- status = module.handle_ajax("get_status", {})
- self.assertTrue(isinstance(status, basestring))
#Mock a student submitting an assessment
assessment_dict = MockQueryDict()
@@ -711,8 +844,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
- status = module.handle_ajax("get_status", {})
- self.assertTrue(isinstance(status, basestring))
#Move to the next step in the problem
module.handle_ajax("next_problem", {})
diff --git a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
index 19e156a0f3..8347b71076 100644
--- a/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
+++ b/common/lib/xmodule/xmodule/tests/test_crowdsource_hinter.py
@@ -2,7 +2,7 @@
Tests the crowdsourced hinter xmodule.
"""
-from mock import Mock
+from mock import Mock, MagicMock
import unittest
import copy
@@ -53,6 +53,7 @@ class CHModuleFactory(object):
@staticmethod
def create(hints=None,
previous_answers=None,
+ user_submissions=None,
user_voted=None,
moderate=None,
mod_queue=None):
@@ -85,17 +86,59 @@ class CHModuleFactory(object):
else:
model_data['previous_answers'] = [
['24.0', [0, 3, 4]],
- ['29.0', [None, None, None]]
+ ['29.0', []]
]
+ if user_submissions is not None:
+ model_data['user_submissions'] = user_submissions
+ else:
+ model_data['user_submissions'] = ['24.0', '29.0']
+
if user_voted is not None:
model_data['user_voted'] = user_voted
if moderate is not None:
model_data['moderate'] = moderate
- descriptor = Mock(weight="1")
+ descriptor = Mock(weight='1')
+ # Make the descriptor have a capa problem child.
+ capa_descriptor = MagicMock()
+ capa_descriptor.name = 'capa'
+ descriptor.get_children = lambda: [capa_descriptor]
+
+ # Make a fake capa module.
+ capa_module = MagicMock()
+ capa_module.lcp = MagicMock()
+ responder = MagicMock()
+
+ def validate_answer(answer):
+ """ A mock answer validator - simulates a numerical response"""
+ try:
+ float(answer)
+ return True
+ except ValueError:
+ return False
+ responder.validate_answer = validate_answer
+
+ def compare_answer(ans1, ans2):
+ """ A fake answer comparer """
+ return ans1 == ans2
+ responder.compare_answer = compare_answer
+
+ capa_module.lcp.responders = {'responder0': responder}
+ capa_module.displayable_items = lambda: [capa_module]
+
system = get_test_system()
+ # Make the system have a marginally-functional get_module
+
+ def fake_get_module(descriptor):
+ """
+ A fake module-maker.
+ """
+ if descriptor.name == 'capa':
+ return capa_module
+ system.get_module = fake_get_module
+
module = CrowdsourceHinterModule(system, descriptor, model_data)
return module
@@ -146,11 +189,13 @@ class VerticalWithModulesFactory(object):
@staticmethod
def next_num():
+ """Increments a global counter for naming."""
CHModuleFactory.num += 1
return CHModuleFactory.num
@staticmethod
def create():
+ """Make a vertical."""
model_data = {'data': VerticalWithModulesFactory.sample_problem_xml}
system = get_test_system()
descriptor = VerticalDescriptor.from_xml(VerticalWithModulesFactory.sample_problem_xml, system)
@@ -226,6 +271,24 @@ class CrowdsourceHinterTest(unittest.TestCase):
self.assertTrue('Test numerical problem.' in out_html)
self.assertTrue('Another test numerical problem.' in out_html)
+ def test_numerical_answer_to_str(self):
+ """
+ Tests the get request to string converter for numerical responses.
+ """
+ mock_module = CHModuleFactory.create()
+ get = {'response1': '4'}
+ parsed = mock_module.numerical_answer_to_str(get)
+ self.assertTrue(parsed == '4')
+
+ def test_formula_answer_to_str(self):
+ """
+ Tests the get request to string converter for formula responses.
+ """
+ mock_module = CHModuleFactory.create()
+ get = {'response1': 'x*y^2'}
+ parsed = mock_module.formula_answer_to_str(get)
+ self.assertTrue(parsed == 'x*y^2')
+
def test_gethint_0hint(self):
"""
Someone asks for a hint, when there's no hint to give.
@@ -235,21 +298,36 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module = CHModuleFactory.create()
json_in = {'problem_name': '26.0'}
out = mock_module.get_hint(json_in)
+ print mock_module.previous_answers
self.assertTrue(out is None)
- self.assertTrue(['26.0', [None, None, None]] in mock_module.previous_answers)
+ self.assertTrue('26.0' in mock_module.user_submissions)
def test_gethint_unparsable(self):
"""
- Someone submits a hint that cannot be parsed into a float.
+ Someone submits an answer that is in the wrong format.
- The answer should not be added to previous_answers.
"""
mock_module = CHModuleFactory.create()
old_answers = copy.deepcopy(mock_module.previous_answers)
- json_in = {'problem_name': 'fish'}
+ json_in = 'blah'
out = mock_module.get_hint(json_in)
self.assertTrue(out is None)
self.assertTrue(mock_module.previous_answers == old_answers)
+ def test_gethint_signature_error(self):
+ """
+ Someone submits an answer that cannot be calculated as a float.
+ Nothing should change.
+ """
+ mock_module = CHModuleFactory.create()
+ old_answers = copy.deepcopy(mock_module.previous_answers)
+ old_user_submissions = copy.deepcopy(mock_module.user_submissions)
+ json_in = {'problem1': 'fish'}
+ out = mock_module.get_hint(json_in)
+ self.assertTrue(out is None)
+ self.assertTrue(mock_module.previous_answers == old_answers)
+ self.assertTrue(mock_module.user_submissions == old_user_submissions)
+
def test_gethint_1hint(self):
"""
Someone asks for a hint, with exactly one hint in the database.
@@ -258,7 +336,11 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module = CHModuleFactory.create()
json_in = {'problem_name': '25.0'}
out = mock_module.get_hint(json_in)
- self.assertTrue(out['best_hint'] == 'Really popular hint')
+ self.assertTrue('Really popular hint' in out['hints'])
+ # Also make sure that the input gets added to user_submissions,
+ # and that the hint is logged in previous_answers.
+ self.assertTrue('25.0' in mock_module.user_submissions)
+ self.assertTrue(['25.0', ['1']] in mock_module.previous_answers)
def test_gethint_manyhints(self):
"""
@@ -271,18 +353,18 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module = CHModuleFactory.create()
json_in = {'problem_name': '24.0'}
out = mock_module.get_hint(json_in)
- self.assertTrue(out['best_hint'] == 'Best hint')
- self.assertTrue('rand_hint_1' in out)
- self.assertTrue('rand_hint_2' in out)
+ self.assertTrue('Best hint' in out['hints'])
+ self.assertTrue(len(out['hints']) == 3)
def test_getfeedback_0wronganswers(self):
"""
Someone has gotten the problem correct on the first try.
Output should be empty.
"""
- mock_module = CHModuleFactory.create(previous_answers=[])
+ mock_module = CHModuleFactory.create(previous_answers=[], user_submissions=[])
json_in = {'problem_name': '42.5'}
out = mock_module.get_feedback(json_in)
+ print out
self.assertTrue(out is None)
def test_getfeedback_1wronganswer_nohints(self):
@@ -294,9 +376,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module = CHModuleFactory.create(previous_answers=[['26.0', [None, None, None]]])
json_in = {'problem_name': '42.5'}
out = mock_module.get_feedback(json_in)
- print out['index_to_answer']
- self.assertTrue(out['index_to_hints'][0] == [])
- self.assertTrue(out['index_to_answer'][0] == '26.0')
+ self.assertTrue(out['answer_to_hints'] == {'26.0': {}})
def test_getfeedback_1wronganswer_withhints(self):
"""
@@ -307,8 +387,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
mock_module = CHModuleFactory.create(previous_answers=[['24.0', [0, 3, None]]])
json_in = {'problem_name': '42.5'}
out = mock_module.get_feedback(json_in)
- print out['index_to_hints']
- self.assertTrue(len(out['index_to_hints'][0]) == 2)
+ self.assertTrue(len(out['answer_to_hints']['24.0']) == 2)
def test_getfeedback_missingkey(self):
"""
@@ -319,7 +398,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
previous_answers=[['24.0', [0, 100, None]]])
json_in = {'problem_name': '42.5'}
out = mock_module.get_feedback(json_in)
- self.assertTrue(len(out['index_to_hints'][0]) == 1)
+ self.assertTrue(len(out['answer_to_hints']['24.0']) == 1)
def test_vote_nopermission(self):
"""
@@ -327,7 +406,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
Should not change any vote tallies.
"""
mock_module = CHModuleFactory.create(user_voted=True)
- json_in = {'answer': 0, 'hint': 1}
+ json_in = {'answer': '24.0', 'hint': 1, 'pk_list': json.dumps([['24.0', 1], ['24.0', 3]])}
old_hints = copy.deepcopy(mock_module.hints)
mock_module.tally_vote(json_in)
self.assertTrue(mock_module.hints == old_hints)
@@ -339,19 +418,56 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
mock_module = CHModuleFactory.create(
previous_answers=[['24.0', [0, 3, None]]])
- json_in = {'answer': 0, 'hint': 3}
+ json_in = {'answer': '24.0', 'hint': 3, 'pk_list': json.dumps([['24.0', 0], ['24.0', 3]])}
dict_out = mock_module.tally_vote(json_in)
self.assertTrue(mock_module.hints['24.0']['0'][1] == 40)
self.assertTrue(mock_module.hints['24.0']['3'][1] == 31)
self.assertTrue(['Best hint', 40] in dict_out['hint_and_votes'])
self.assertTrue(['Another hint', 31] in dict_out['hint_and_votes'])
+ def test_vote_unparsable(self):
+ """
+ A user somehow votes for an unparsable answer.
+ Should return a friendly error.
+ (This is an unusual exception path - I don't know how it occurs,
+ except if you manually make a post request. But, it seems to happen
+ occasionally.)
+ """
+ mock_module = CHModuleFactory.create()
+ # None means that the answer couldn't be parsed.
+ mock_module.answer_signature = lambda text: None
+ json_in = {'answer': 'fish', 'hint': 3, 'pk_list': '[]'}
+ dict_out = mock_module.tally_vote(json_in)
+ print dict_out
+ self.assertTrue(dict_out == {'error': 'Failure in voting!'})
+
+ def test_vote_nohint(self):
+ """
+ A user somehow votes for a hint that doesn't exist.
+ Should return a friendly error.
+ """
+ mock_module = CHModuleFactory.create()
+ json_in = {'answer': '24.0', 'hint': '25', 'pk_list': '[]'}
+ dict_out = mock_module.tally_vote(json_in)
+ self.assertTrue(dict_out == {'error': 'Failure in voting!'})
+
+ def test_vote_badpklist(self):
+ """
+ Some of the pk's specified in pk_list are invalid.
+ Should just skip those.
+ """
+ mock_module = CHModuleFactory.create()
+ json_in = {'answer': '24.0', 'hint': '0', 'pk_list': json.dumps([['24.0', 0], ['24.0', 12]])}
+ hint_and_votes = mock_module.tally_vote(json_in)['hint_and_votes']
+ self.assertTrue(['Best hint', 41] in hint_and_votes)
+ self.assertTrue(len(hint_and_votes) == 1)
+
def test_submithint_nopermission(self):
"""
A user tries to submit a hint, but he has already voted.
"""
mock_module = CHModuleFactory.create(user_voted=True)
- json_in = {'answer': 1, 'hint': 'This is a new hint.'}
+ json_in = {'answer': '29.0', 'hint': 'This is a new hint.'}
print mock_module.user_voted
mock_module.submit_hint(json_in)
print mock_module.hints
@@ -363,7 +479,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
exist yet.
"""
mock_module = CHModuleFactory.create()
- json_in = {'answer': 1, 'hint': 'This is a new hint.'}
+ json_in = {'answer': '29.0', 'hint': 'This is a new hint.'}
mock_module.submit_hint(json_in)
self.assertTrue('29.0' in mock_module.hints)
@@ -373,13 +489,12 @@ class CrowdsourceHinterTest(unittest.TestCase):
already.
"""
mock_module = CHModuleFactory.create(previous_answers=[['25.0', [1, None, None]]])
- json_in = {'answer': 0, 'hint': 'This is a new hint.'}
+ json_in = {'answer': '25.0', 'hint': 'This is a new hint.'}
mock_module.submit_hint(json_in)
# Make a hint request.
json_in = {'problem name': '25.0'}
out = mock_module.get_hint(json_in)
- self.assertTrue((out['best_hint'] == 'This is a new hint.')
- or (out['rand_hint_1'] == 'This is a new hint.'))
+ self.assertTrue('This is a new hint.' in out['hints'])
def test_submithint_moderate(self):
"""
@@ -388,7 +503,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
dict.
"""
mock_module = CHModuleFactory.create(moderate='True')
- json_in = {'answer': 1, 'hint': 'This is a new hint.'}
+ json_in = {'answer': '29.0', 'hint': 'This is a new hint.'}
mock_module.submit_hint(json_in)
self.assertTrue('29.0' not in mock_module.hints)
self.assertTrue('29.0' in mock_module.mod_queue)
@@ -398,10 +513,20 @@ class CrowdsourceHinterTest(unittest.TestCase):
Make sure that hints are being html-escaped.
"""
mock_module = CHModuleFactory.create()
- json_in = {'answer': 1, 'hint': ''}
+ json_in = {'answer': '29.0', 'hint': ''}
mock_module.submit_hint(json_in)
+ self.assertTrue(mock_module.hints['29.0']['0'][0] == u'<script> alert("Trololo"); </script>')
+
+ def test_submithint_unparsable(self):
+ mock_module = CHModuleFactory.create()
+ mock_module.answer_signature = lambda text: None
+ json_in = {'answer': 'fish', 'hint': 'A hint'}
+ dict_out = mock_module.submit_hint(json_in)
+ print dict_out
print mock_module.hints
- self.assertTrue(mock_module.hints['29.0'][0][0] == u'<script> alert("Trololo"); </script>')
+ self.assertTrue('error' in dict_out)
+ self.assertTrue(None not in mock_module.hints)
+ self.assertTrue('fish' not in mock_module.hints)
def test_template_gethint(self):
"""
@@ -409,7 +534,7 @@ class CrowdsourceHinterTest(unittest.TestCase):
"""
mock_module = CHModuleFactory.create()
- def fake_get_hint(get):
+ def fake_get_hint(_):
"""
Creates a rendering dictionary, with which we can test
the templates.
diff --git a/common/lib/xmodule/xmodule/tests/test_date_utils.py b/common/lib/xmodule/xmodule/tests/test_date_utils.py
index bacc5de91b..37f30e1b56 100644
--- a/common/lib/xmodule/xmodule/tests/test_date_utils.py
+++ b/common/lib/xmodule/xmodule/tests/test_date_utils.py
@@ -1,6 +1,6 @@
"""Tests for xmodule.util.date_utils"""
-from nose.tools import assert_equals, assert_false
+from nose.tools import assert_equals, assert_false # pylint: disable=E0611
from xmodule.util.date_utils import get_default_time_display, almost_same_datetime
from datetime import datetime, timedelta, tzinfo
from pytz import UTC
diff --git a/common/lib/xmodule/xmodule/tests/test_export.py b/common/lib/xmodule/xmodule/tests/test_export.py
index 5c5d8307af..b0c1617580 100644
--- a/common/lib/xmodule/xmodule/tests/test_export.py
+++ b/common/lib/xmodule/xmodule/tests/test_export.py
@@ -6,6 +6,8 @@ from datetime import datetime, timedelta, tzinfo
from tempfile import mkdtemp
import unittest
import shutil
+from textwrap import dedent
+import mock
import pytz
from fs.osfs import OSFS
@@ -35,12 +37,23 @@ def strip_filenames(descriptor):
class RoundTripTestCase(unittest.TestCase):
- ''' Check that our test courses roundtrip properly.
- Same course imported , than exported, then imported again.
- And we compare original import with second import (after export).
- Thus we make sure that export and import work properly.
- '''
- def check_export_roundtrip(self, data_dir, course_dir):
+ """
+ Check that our test courses roundtrip properly.
+ Same course imported , than exported, then imported again.
+ And we compare original import with second import (after export).
+ Thus we make sure that export and import work properly.
+ """
+
+ @mock.patch('xmodule.course_module.requests.get')
+ def check_export_roundtrip(self, data_dir, course_dir, mock_get):
+
+ # Patch network calls to retrieve the textbook TOC
+ mock_get.return_value.text = dedent("""
+
+
+
+ """).strip()
+
root_dir = path(self.temp_dir)
print("Copying test course to temp dir {0}".format(root_dir))
diff --git a/common/lib/xmodule/xmodule/tests/test_peer_grading.py b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
index fcdb0bb1ac..240fef4e87 100644
--- a/common/lib/xmodule/xmodule/tests/test_peer_grading.py
+++ b/common/lib/xmodule/xmodule/tests/test_peer_grading.py
@@ -61,7 +61,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Try getting data from the external grading service
@return:
"""
- success, data = self.peer_grading.query_data_for_location()
+ success, data = self.peer_grading.query_data_for_location(self.problem_location.url())
self.assertEqual(success, True)
def test_get_score(self):
diff --git a/common/lib/xmodule/xmodule/tests/test_stringify.py b/common/lib/xmodule/xmodule/tests/test_stringify.py
index 49852ee233..10abaf72f5 100644
--- a/common/lib/xmodule/xmodule/tests/test_stringify.py
+++ b/common/lib/xmodule/xmodule/tests/test_stringify.py
@@ -1,4 +1,4 @@
-from nose.tools import assert_equals
+from nose.tools import assert_equals # pylint: disable=E0611
from lxml import etree
from xmodule.stringify import stringify_children
diff --git a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
index c717d52d31..4e2657a2c0 100644
--- a/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_util_open_ended.py
@@ -52,3 +52,27 @@ class DummyModulestore(object):
location = Location(location)
descriptor = self.modulestore.get_instance(course.id, location, depth=None)
return descriptor.xmodule(self.test_system)
+
+# Task state for a module with self assessment then instructor assessment.
+TEST_STATE_SA_IN = ["{\"child_created\": false, \"child_attempts\": 2, \"version\": 1, \"child_history\": [{\"answer\": \"However venture pursuit he am mr cordial. Forming musical am hearing studied be luckily. Ourselves for determine attending how led gentleman sincerity. Valley afford uneasy joy she thrown though bed set. In me forming general prudent on country carried. Behaved an or suppose justice. Seemed whence how son rather easily and change missed. Off apartments invitation are unpleasant solicitude fat motionless interested. Hardly suffer wisdom wishes valley as an. As friendship advantages resolution it alteration stimulated he or increasing. \\r
Now led tedious shy lasting females off. Dashwood marianne in of entrance be on wondered possible building. Wondered sociable he carriage in speedily margaret. Up devonshire of he thoroughly insensible alteration. An mr settling occasion insisted distance ladyship so. Not attention say frankness intention out dashwoods now curiosity. Stronger ecstatic as no judgment daughter speedily thoughts. Worse downs nor might she court did nay forth these. \", \"post_assessment\": \"[3, 3, 2, 2, 2]\", \"score\": 12}, {\"answer\": \"Delightful remarkably mr on announcing themselves entreaties favourable. About to in so terms voice at. Equal an would is found seems of. The particular friendship one sufficient terminated frequently themselves. It more shed went up is roof if loud case. Delay music in lived noise an. Beyond genius really enough passed is up. \\r
John draw real poor on call my from. May she mrs furnished discourse extremely. Ask doubt noisy shade guest did built her him. Ignorant repeated hastened it do. Consider bachelor he yourself expenses no. Her itself active giving for expect vulgar months. Discovery commanded fat mrs remaining son she principle middleton neglected. Be miss he in post sons held. No tried is defer do money scale rooms. \", \"post_assessment\": \"[3, 3, 2, 2, 2]\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"However venture pursuit he am mr cordial. Forming musical am hearing studied be luckily. Ourselves for determine attending how led gentleman sincerity. Valley afford uneasy joy she thrown though bed set. In me forming general prudent on country carried. Behaved an or suppose justice. Seemed whence how son rather easily and change missed. Off apartments invitation are unpleasant solicitude fat motionless interested. Hardly suffer wisdom wishes valley as an. As friendship advantages resolution it alteration stimulated he or increasing. \\r
Now led tedious shy lasting females off. Dashwood marianne in of entrance be on wondered possible building. Wondered sociable he carriage in speedily margaret. Up devonshire of he thoroughly insensible alteration. An mr settling occasion insisted distance ladyship so. Not attention say frankness intention out dashwoods now curiosity. Stronger ecstatic as no judgment daughter speedily thoughts. Worse downs nor might she court did nay forth these. \", \"post_assessment\": \"{\\\"submission_id\\\": 1460, \\\"score\\\": 12, \\\"feedback\\\": \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 5413, \\\"grader_type\\\": \\\"IN\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"\\\\nIdeas\\\\n3\\\\nContent\\\\n3\\\\nOrganization\\\\n2\\\\nStyle\\\\n2\\\\nVoice\\\\n2\\\"}\", \"score\": 12}, {\"answer\": \"Delightful remarkably mr on announcing themselves entreaties favourable. About to in so terms voice at. Equal an would is found seems of. The particular friendship one sufficient terminated frequently themselves. It more shed went up is roof if loud case. Delay music in lived noise an. Beyond genius really enough passed is up. \\r
John draw real poor on call my from. May she mrs furnished discourse extremely. Ask doubt noisy shade guest did built her him. Ignorant repeated hastened it do. Consider bachelor he yourself expenses no. Her itself active giving for expect vulgar months. Discovery commanded fat mrs remaining son she principle middleton neglected. Be miss he in post sons held. No tried is defer do money scale rooms. \", \"post_assessment\": \"{\\\"submission_id\\\": 1462, \\\"score\\\": 12, \\\"feedback\\\": \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 5418, \\\"grader_type\\\": \\\"IN\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"\\\\nIdeas\\\\n3\\\\nContent\\\\n3\\\\nOrganization\\\\n2\\\\nStyle\\\\n2\\\\nVoice\\\\n2\\\"}\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"post_assessment\"}"]
+
+# Mock instance state. Should receive a score of 15.
+MOCK_INSTANCE_STATE = r"""{"ready_to_reset": false, "skip_spelling_checks": true, "current_task_number": 1, "weight": 5.0, "graceperiod": "1 day 12 hours 59 minutes 59 seconds", "graded": "True", "task_states": ["{\"child_created\": false, \"child_attempts\": 4, \"version\": 1, \"child_history\": [{\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"[3]\", \"score\": 3}, {\"answer\": \"\", \"post_assessment\": \"[3]\", \"score\": 3}], \"max_score\": 3, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"The students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the group\\u2019s procedure, describe what additional information you would need in order to replicate the expe\", \"post_assessment\": \"{\\\"submission_id\\\": 3097, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: More grammar errors than average.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the groups procedure , describe what additional information you would need in order to replicate the expe\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3233, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"After 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\\r\\nStarting Mass (g)\\tEnding Mass (g)\\tDifference in Mass (g)\\r\\nMarble\\t 9.8\\t 9.4\\t\\u20130.4\\r\\nLimestone\\t10.4\\t 9.1\\t\\u20131.3\\r\\nWood\\t11.2\\t11.2\\t 0.0\\r\\nPlastic\\t 7.2\\t 7.1\\t\\u20130.1\\r\\nAfter reading the\", \"post_assessment\": \"{\\\"submission_id\\\": 3098, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . starting mass g ending mass g difference in mass g marble . . . limestone . . . wood . . . plastic . . . after reading the\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3235, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"To replicate the experiment, the procedure would require more detail. One piece of information that is omitted is the amount of vinegar used in the experiment. It is also important to know what temperature the experiment was kept at during the 24 hours. Finally, the procedure needs to include details about the experiment, for example if the whole sample must be submerged.\", \"post_assessment\": \"{\\\"submission_id\\\": 3099, \\\"score\\\": 3, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"to replicate the experiment , the procedure would require more detail . one piece of information that is omitted is the amount of vinegar used in the experiment . it is also important to know what temperature the experiment was kept at during the hours . finally , the procedure needs to include details about the experiment , for example if the whole sample must be submerged .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3237, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality3\\\"}\", \"score\": 3}, {\"answer\": \"e the mass of four different samples.\\r\\nPour vinegar in each of four separate, but identical, containers.\\r\\nPlace a sample of one material into one container and label. Repeat with remaining samples, placing a single sample into a single container.\\r\\nAfter 24 hours, remove the samples from the containers and rinse each sample with distilled water.\\r\\nAllow the samples to sit and dry for 30 minutes.\\r\\nDetermine the mass of each sample.\\r\\nThe students\\u2019 data are recorded in the table below.\\r\\n\", \"post_assessment\": \"{\\\"submission_id\\\": 3100, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"e the mass of four different samples . pour vinegar in each of four separate , but identical , containers . place a sample of one material into one container and label . repeat with remaining samples , placing a single sample into a single container . after hours , remove the samples from the containers and rinse each sample with distilled water . allow the samples to sit and dry for minutes . determine the mass of each sample . the students data are recorded in the table below . \\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3239, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}, {\"answer\": \"\", \"post_assessment\": \"{\\\"submission_id\\\": 3101, \\\"score\\\": 0, \\\"feedback\\\": \\\"{\\\\\\\"spelling\\\\\\\": \\\\\\\"Spelling: Ok.\\\\\\\", \\\\\\\"grammar\\\\\\\": \\\\\\\"Grammar: Ok.\\\\\\\", \\\\\\\"markup-text\\\\\\\": \\\\\\\"invalid essay .\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 3241, \\\"grader_type\\\": \\\"ML\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Response Quality0\\\"}\", \"score\": 0}], \"max_score\": 3, \"child_state\": \"done\"}"], "attempts": "10000", "student_attempts": 0, "due": null, "state": "done", "accept_file_upload": false, "display_name": "Science Question -- Machine Assessed"}"""
+
+# Task state with self assessment only.
+TEST_STATE_SA = ["{\"child_created\": false, \"child_attempts\": 1, \"version\": 1, \"child_history\": [{\"answer\": \"Censorship in the Libraries\\r 'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author\\r
Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.\", \"post_assessment\": \"[3, 3, 2, 2, 2]\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"Censorship in the Libraries\\r 'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author\\r
Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.\", \"post_assessment\": \"{\\\"submission_id\\\": 1461, \\\"score\\\": 12, \\\"feedback\\\": \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 5414, \\\"grader_type\\\": \\\"IN\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"\\\\nIdeas\\\\n3\\\\nContent\\\\n3\\\\nOrganization\\\\n2\\\\nStyle\\\\n2\\\\nVoice\\\\n2\\\"}\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"post_assessment\"}"]
+
+# Task state with self and then ai assessment.
+TEST_STATE_AI = ["{\"child_created\": false, \"child_attempts\": 2, \"version\": 1, \"child_history\": [{\"answer\": \"In libraries, there should not be censorship on materials considering that it's an individual's decision to read what they prefer. There is no appropriate standard on what makes a book offensive to a group, so it should be undetermined as to what makes a book offensive. In a public library, many children, who the books are censored for, are with their parents. Parents should make an independent choice on what they can allow their children to read. Letting society ban a book simply for the use of inappropriate materials is ridiculous. If an author spent time creating a story, it should be appreciated, and should not put on a list of no-nos. If a certain person doesn't like a book's reputation, all they have to do is not read it. Even in school systems, librarians are there to guide kids to read good books. If a child wants to read an inappropriate book, the librarian will most likely discourage him or her not to read it. In my experience, I wanted to read a book that my mother suggested to me, but as I went to the school library it turned out to be a censored book. Some parents believe children should be ignorant about offensive things written in books, but honestly many of the same ideas are exploited to them everyday on television and internet. So trying to shield your child from the bad things may be a great thing, but the efforts are usually failed attempts. It also never occurs to the people censoring the books, that some people can't afford to buy the books they want to read. The libraries, for some, are the main means for getting books. To conclude there is very little reason to ban a book from the shelves. Many of the books banned have important lessons that can be obtained through reading it. If a person doesn't like a book, the simplest thing to do is not to pick it up.\", \"post_assessment\": \"[1, 1]\", \"score\": 2}, {\"answer\": \"This is another response\", \"post_assessment\": \"[1, 1]\", \"score\": 2}], \"max_score\": 2, \"child_state\": \"done\"}", "{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"In libraries, there should not be censorship on materials considering that it's an individual's decision to read what they prefer. There is no appropriate standard on what makes a book offensive to a group, so it should be undetermined as to what makes a book offensive. In a public library, many children, who the books are censored for, are with their parents. Parents should make an independent choice on what they can allow their children to read. Letting society ban a book simply for the use of inappropriate materials is ridiculous. If an author spent time creating a story, it should be appreciated, and should not put on a list of no-nos. If a certain person doesn't like a book's reputation, all they have to do is not read it. Even in school systems, librarians are there to guide kids to read good books. If a child wants to read an inappropriate book, the librarian will most likely discourage him or her not to read it. In my experience, I wanted to read a book that my mother suggested to me, but as I went to the school library it turned out to be a censored book. Some parents believe children should be ignorant about offensive things written in books, but honestly many of the same ideas are exploited to them everyday on television and internet. So trying to shield your child from the bad things may be a great thing, but the efforts are usually failed attempts. It also never occurs to the people censoring the books, that some people can't afford to buy the books they want to read. The libraries, for some, are the main means for getting books. To conclude there is very little reason to ban a book from the shelves. Many of the books banned have important lessons that can be obtained through reading it. If a person doesn't like a book, the simplest thing to do is not to pick it up.\", \"post_assessment\": \"{\\\"submission_id\\\": 6107, \\\"score\\\": 2, \\\"feedback\\\": \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"success\\\": true, \\\"grader_id\\\": 1898718, \\\"grader_type\\\": \\\"IN\\\", \\\"rubric_scores_complete\\\": true, \\\"rubric_xml\\\": \\\"Writing Applications1 Language Conventions 1\\\"}\", \"score\": 2}, {\"answer\": \"This is another response\"}], \"max_score\": 2, \"child_state\": \"assessing\"}"]
+
+# Task state with ai assessment only.
+TEST_STATE_AI2 = ["{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"This isn't a real essay, and you should give me a zero on it. \", \"post_assessment\": \"{\\\"submission_id\\\": 18446, \\\"score\\\": [0, 1, 0], \\\"feedback\\\": [\\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"Zero it is! \\\\\\\"}\\\"], \\\"success\\\": true, \\\"grader_id\\\": [1944146, 1943188, 1940991], \\\"grader_type\\\": \\\"PE\\\", \\\"rubric_scores_complete\\\": [true, true, true], \\\"rubric_xml\\\": [\\\"Writing Applications0 Language Conventions 0\\\", \\\"Writing Applications0 Language Conventions 1\\\", \\\"Writing Applications0 Language Conventions 0\\\"]}\", \"score\": 0}], \"max_score\": 2, \"child_state\": \"post_assessment\"}"]
+
+# Invalid task state with ai assessment.
+TEST_STATE_AI2_INVALID = ["{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"This isn't a real essay, and you should give me a zero on it. \", \"post_assessment\": \"{\\\"submission_id\\\": 18446, \\\"score\\\": [0, 1, 0], \\\"feedback\\\": [\\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\", \\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"Zero it is! \\\\\\\"}\\\"], \\\"success\\\": true, \\\"grader_id\\\": [1943188, 1940991], \\\"grader_type\\\": \\\"PE\\\", \\\"rubric_scores_complete\\\": [true, true, true], \\\"rubric_xml\\\": [\\\"Writing Applications0 Language Conventions 0\\\", \\\"Writing Applications0 Language Conventions 1\\\", \\\"Writing Applications0 Language Conventions 0\\\"]}\", \"score\": 0}], \"max_score\": 2, \"child_state\": \"post_assessment\"}"]
+
+# Self assessment state.
+TEST_STATE_SINGLE = ["{\"child_created\": false, \"child_attempts\": 1, \"version\": 1, \"child_history\": [{\"answer\": \"'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author\\r
Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading. \", \"post_assessment\": \"[3, 3, 2, 2, 2]\", \"score\": 12}], \"max_score\": 12, \"child_state\": \"done\"}"]
+
+# Peer grading state.
+TEST_STATE_PE_SINGLE = ["{\"child_created\": false, \"child_attempts\": 0, \"version\": 1, \"child_history\": [{\"answer\": \"Passage its ten led hearted removal cordial. Preference any astonished unreserved mrs. Prosperous understood middletons in conviction an uncommonly do. Supposing so be resolving breakfast am or perfectly. Is drew am hill from mr. Valley by oh twenty direct me so. Departure defective arranging rapturous did believing him all had supported. Family months lasted simple set nature vulgar him. Picture for attempt joy excited ten carried manners talking how. Suspicion neglected he resolving agreement perceived at an. \\r
Ye on properly handsome returned throwing am no whatever. In without wishing he of picture no exposed talking minutes. Curiosity continual belonging offending so explained it exquisite. Do remember to followed yourself material mr recurred carriage. High drew west we no or at john. About or given on witty event. Or sociable up material bachelor bringing landlord confined. Busy so many in hung easy find well up. So of exquisite my an explained remainder. Dashwood denoting securing be on perceive my laughing so. \\r
Ought these are balls place mrs their times add she. Taken no great widow spoke of it small. Genius use except son esteem merely her limits. Sons park by do make on. It do oh cottage offered cottage in written. Especially of dissimilar up attachment themselves by interested boisterous. Linen mrs seems men table. Jennings dashwood to quitting marriage bachelor in. On as conviction in of appearance apartments boisterous. \", \"post_assessment\": \"{\\\"submission_id\\\": 1439, \\\"score\\\": [0], \\\"feedback\\\": [\\\"{\\\\\\\"feedback\\\\\\\": \\\\\\\"\\\\\\\"}\\\"], \\\"success\\\": true, \\\"grader_id\\\": [5337], \\\"grader_type\\\": \\\"PE\\\", \\\"rubric_scores_complete\\\": [true], \\\"rubric_xml\\\": [\\\"\\\\nIdeas\\\\n0\\\\nContent\\\\n0\\\\nOrganization\\\\n0\\\\nStyle\\\\n0\\\\nVoice\\\\n0\\\"]}\", \"score\": 0}], \"max_score\": 12, \"child_state\": \"done\"}"]
\ No newline at end of file
diff --git a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
index c98f980c62..0a97728ec7 100644
--- a/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
+++ b/common/lib/xmodule/xmodule/tests/test_xblock_wrappers.py
@@ -3,7 +3,7 @@ Tests for the wrapping layer that provides the XBlock API using XModule/Descript
functionality
"""
-from nose.tools import assert_equal
+from nose.tools import assert_equal # pylint: disable=E0611
from unittest.case import SkipTest
from mock import Mock
diff --git a/common/lib/xmodule/xmodule/tests/test_xml_module.py b/common/lib/xmodule/xmodule/tests/test_xml_module.py
index fdcff8ade2..3463140555 100644
--- a/common/lib/xmodule/xmodule/tests/test_xml_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_xml_module.py
@@ -7,7 +7,7 @@ from xmodule.fields import Date, Timedelta
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
import unittest
from .import get_test_system
-from nose.tools import assert_equals
+from nose.tools import assert_equals # pylint: disable=E0611
from mock import Mock
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index 953447b2f2..d846777360 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -24,9 +24,6 @@ from xmodule.editing_module import TabsEditingDescriptor
from xmodule.raw_module import EmptyDataRawDescriptor
from xmodule.xml_module import is_pointer_tag, name_to_pathname
from xmodule.modulestore import Location
-from xmodule.modulestore.mongo import MongoModuleStore
-from xmodule.modulestore.django import modulestore
-from xmodule.contentstore.content import StaticContent
from xblock.core import Scope, String, Boolean, Float, List, Integer
import datetime
@@ -104,14 +101,14 @@ class VideoFields(object):
default=[]
)
track = String(
- help="The external URL to download the subtitle track. This appears as a link beneath the video.",
+ help="The external URL to download the timed transcript track. This appears as a link beneath the video.",
display_name="Download Track",
scope=Scope.settings,
default=""
)
sub = String(
- help="The name of the subtitle track (for non-Youtube videos).",
- display_name="HTML5 Subtitles",
+ help="The name of the timed transcript track (for non-Youtube videos).",
+ display_name="HTML5 Timed Transcript",
scope=Scope.settings,
default=""
)
diff --git a/cms/static/css/tiny-mce.css b/common/static/css/tiny-mce.css
similarity index 100%
rename from cms/static/css/tiny-mce.css
rename to common/static/css/tiny-mce.css
diff --git a/common/static/js/capa/spec/formula_equation_preview_spec.js b/common/static/js/capa/spec/formula_equation_preview_spec.js
index 39151f0f63..720a0a3e91 100644
--- a/common/static/js/capa/spec/formula_equation_preview_spec.js
+++ b/common/static/js/capa/spec/formula_equation_preview_spec.js
@@ -15,11 +15,14 @@ describe("Formula Equation Preview", function () {
var $fixture = this.$fixture = $('\
\
\
- \
-
- ${_("Student activity day by day")}
+ ${_("Student activity day by day")}
(${analytics_results["StudentsDropoffPerDay"]['time']})
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index e8b676e014..31c3c33ef1 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -5,6 +5,8 @@
from courseware.courses import course_image_url, get_course_about_section
from courseware.access import has_access
from certificates.models import CertificateStatuses
+ from xmodule.modulestore import MONGO_MODULESTORE_TYPE
+ from xmodule.modulestore.django import modulestore
%>
<%inherit file="main.html" />
@@ -16,6 +18,14 @@
%block>
@@ -280,6 +308,10 @@
% endif
% endif
${_('Unregister')}
+ % if settings.MITX_FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and modulestore().get_modulestore_type(course.id) == MONGO_MODULESTORE_TYPE:
+
+ ${_('Email Settings')}
+ % endif
@@ -313,6 +345,29 @@
+
+
+
+
${_('Email Settings for {course_number}').format(course_number='')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
✕
+
+
+
+
+
diff --git a/lms/templates/emails/enroll_email_allowedmessage.txt b/lms/templates/emails/enroll_email_allowedmessage.txt
index eab347166e..b7d65f0f2c 100644
--- a/lms/templates/emails/enroll_email_allowedmessage.txt
+++ b/lms/templates/emails/enroll_email_allowedmessage.txt
@@ -2,7 +2,7 @@ Dear student,
You have been invited to join ${course_id} at ${site_name} by a member of the course staff.
-To finish your registration, please visit ${registration_url} and fill out the registration form.
+To finish your registration, please visit ${registration_url} and fill out the registration form making sure to use ${email_address} in the E-mail field.
% if auto_enroll:
Once you have registered and activated your account, you will see ${course_id} listed on your dashboard.
% else:
diff --git a/lms/templates/emails/order_confirmation_email.txt b/lms/templates/emails/order_confirmation_email.txt
new file mode 100644
index 0000000000..4bbc70491b
--- /dev/null
+++ b/lms/templates/emails/order_confirmation_email.txt
@@ -0,0 +1,15 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+${_("Thank you for your order! Payment was successful, and you should be able to see the results on your dashboard.")}
+
+${_("Your order number is: {order_number}").format(order_number=order.id)}
+
+${_("Items in your order:")}
+
+${_("Quantity - Description - Price")}
+%for order_item in order_items:
+ ${order_item.qty} - ${order_item.line_desc} - $(order_item.line_cost}
+%endfor
+${_("Total: {total_cost}").format(total_cost=order.total_cost)}
+
+${_("If you have any issues, please contact us at {billing_email}").format(billing_email=settings.PAYMENT_SUPPORT_EMAIL)}
diff --git a/lms/templates/courseware/hint_manager.html b/lms/templates/instructor/hint_manager.html
similarity index 98%
rename from lms/templates/courseware/hint_manager.html
rename to lms/templates/instructor/hint_manager.html
index ebd7091a09..4a6e219560 100644
--- a/lms/templates/courseware/hint_manager.html
+++ b/lms/templates/instructor/hint_manager.html
@@ -1,6 +1,6 @@
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
-<%namespace name="content" file="/courseware/hint_manager_inner.html"/>
+<%namespace name="content" file="/instructor/hint_manager_inner.html"/>
<%block name="headextra">
diff --git a/lms/templates/courseware/hint_manager_inner.html b/lms/templates/instructor/hint_manager_inner.html
similarity index 95%
rename from lms/templates/courseware/hint_manager_inner.html
rename to lms/templates/instructor/hint_manager_inner.html
index c69539522f..45101be2f6 100644
--- a/lms/templates/courseware/hint_manager_inner.html
+++ b/lms/templates/instructor/hint_manager_inner.html
@@ -4,6 +4,7 @@
${_("Here are a list of problems that need to be peer graded for this course.")}
- % if success:
- % if len(problem_list) == 0:
-
- ${_("Nothing to grade!")}
-
- %else:
-
-
-
-
${_("Problem Name")}
-
${_("Due date")}
-
${_("Graded")}
-
${_("Available")}
-
${_("Required")}
-
${_("Progress")}
+
+
${_("Peer Grading")}
+
${_("Instructions")}
+
${_("Here are a list of problems that need to be peer graded for this course.")}
+ % if success:
+ % if len(problem_list) == 0:
+
+ ${_("You currently do not having any peer grading to do. In order to have peer grading to do, you need to have submitted a response to a peer grading problem. The instructor also needs to score the essays that are used to help you better understand the grading criteria.")}
+
${_("Please include some written feedback as well.")}
+
+
${_("This submission has explicit or pornographic content : ")}
+
+
+
${_("I do not know how to grade this question : ")}
+
+
+
+
+
+
+
+
+
+
${_("How did I do?")}
+
+
+
-
+
+
+
${_("Ready to grade!")}
+
${_("You have finished learning to grade, which means that you are now ready to start grading.")}
+
+
+
+
+
${_("Learning to grade")}
+
${_("You have not yet finished learning to grade this problem.")}
+
${_("You will now be shown a series of instructor-scored essays, and will be asked to score them yourself.")}
+
${_("Once you can score the essays similarly to an instructor, you will be ready to grade your peers.")}
+
+
-
-
${_("Student Response")}
-
-
-
-
-
+
+
+
${_("Are you sure that you want to flag this submission?")}
+
+ ${_("You are about to flag a submission. You should only flag a submission that contains explicit or offensive content. If the submission is not addressed to the question or is incorrect, you should give it a score of zero and accompanying feedback instead of flagging it.")}
+
+
+
+
-
-
-
-
-
-
-
-
${_("Written Feedback")}
-
${_("Please include some written feedback as well.")}
-
-
${_("This submission has explicit or pornographic content : ")}
-
${_("I do not know how to grade this question : ")}
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
${_("How did I do?")}
-
-
-
-
-
-
-
-
${_("Ready to grade!")}
-
${_("You have finished learning to grade, which means that you are now ready to start grading.")}
-
-
-
-
-
-
${_("Learning to grade")}
-
${_("You have not yet finished learning to grade this problem.")}
-
${_("You will now be shown a series of instructor-scored essays, and will be asked to score them yourself.")}
-
${_("Once you can score the essays similarly to an instructor, you will be ready to grade your peers.")}
-
-
-
-
-
-
${_("Are you sure that you want to flag this submission?")}
-
- ${_("You are about to flag a submission. You should only flag a submission that contains explicit or offensive content. If the submission is not addressed to the question or is incorrect, you should give it a score of zero and accompanying feedback instead of flagging it.")}
-
${_("Fill out the form to the left (all fields are required), and your "
-"account will be created; you'll be sent an email with instructions on how "
-"to finish your registration.")}
-
-
${_("We'll only use your email to send you signup instructions. We hate spam "
-"as much as you do.")}
-
-
${_("This account will let you log into the ticket tracker, claim tickets, "
-"and be exempt from spam filtering")}.
-{% endblock %}
diff --git a/lms/templates/widgets/html-edit.html b/lms/templates/widgets/html-edit.html
new file mode 100644
index 0000000000..0cb0ca4f0a
--- /dev/null
+++ b/lms/templates/widgets/html-edit.html
@@ -0,0 +1,13 @@
+<%! from django.utils.translation import ugettext as _ %>
+
+
+