Commit 42ac76ce by Will Daly

Merge branch 'master' into lapentab/fix_network_tests

Conflicts:
	lms/djangoapps/courseware/tests/tests.py
parents d894a065 c680dfdd
......@@ -2,6 +2,7 @@ Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists
I want to be able to manually enter JSON key /value pairs
Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio
When I select the Advanced Settings
......@@ -11,6 +12,8 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
......@@ -19,6 +22,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is unchanged
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save
......@@ -26,6 +31,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then the policy key value is changed
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value for "discussion_topics"
......@@ -33,6 +40,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then it is displayed as formatted
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test error if value supplied is of the wrong type
Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value for "display_name"
......@@ -41,6 +50,8 @@ Feature: Advanced (manual) course policy
Then the policy key value is unchanged
# This feature will work in Firefox only when Firefox is the active window
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes
......@@ -48,6 +59,8 @@ Feature: Advanced (manual) course policy
And I reload the page
Then it is displayed as a string
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Confirmation is shown on save
Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key
......
......@@ -10,7 +10,10 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist
And They are correctly selected after reloading the page
# CHROME ONLY, due to issues getting link to be active in firefox
# There are issues getting link to be active in browsers other than chrome
@skip_firefox
@skip_internetexplorer
@skip_safari
Scenario: A task can link to a location within Studio
Given I have opened Checklists
When I select a link to the course outline
......@@ -18,7 +21,10 @@ Feature: Course checklists
And I press the browser back button
Then I am brought back to the course outline in the correct state
# CHROME ONLY, due to issues getting link to be active in firefox
# There are issues getting link to be active in browsers other than chrome
@skip_firefox
@skip_internetexplorer
@skip_safari
Scenario: A task can link to a location outside Studio
Given I have opened Checklists
When I select a link to help page
......
......@@ -64,6 +64,10 @@ Feature: Course Overview
And I change an assignment's grading status
Then I am shown a notification
# Notification is not shown on reorder for IE
# Safari does not have moveMouseTo implemented
@skip_internetexplorer
@skip_safari
Scenario: Notification is shown on subsection reorder
Given I have opened a new course section in Studio
And I have added a new subsection
......
Feature: Course Settings
As a course author, I want to be able to configure my course settings.
# Safari has trouble keeps dates on refresh
@skip_safari
Scenario: User can set course dates
Given I have opened a new course in Studio
When I select Schedule and Details
......@@ -8,12 +10,16 @@ Feature: Course Settings
And I press the "Save" notification button
Then I see the set dates on refresh
# IE has trouble with saving information
@skip_internetexplorer
Scenario: User can clear previously set course dates (except start date)
Given I have set course dates
And I clear all the dates except start
And I press the "Save" notification button
Then I see cleared dates on refresh
# IE has trouble with saving information
@skip_internetexplorer
Scenario: User cannot clear the course start date
Given I have set course dates
And I press the "Save" notification button
......@@ -21,6 +27,10 @@ Feature: Course Settings
Then I receive a warning about course start date
And The previously set start date is shown on refresh
# IE has trouble with saving information
# Safari gets CSRF token errors
@skip_internetexplorer
@skip_safari
Scenario: User can correct the course start date warning
Given I have tried to clear the course start
And I have entered a new course start date
......@@ -28,12 +38,16 @@ Feature: Course Settings
Then The warning about course start date goes away
And My new course start date is shown on refresh
# Safari does not save + refresh properly through sauce labs
@skip_safari
Scenario: Settings are only persisted when saved
Given I have set course dates
And I press the "Save" notification button
When I change fields
Then I do not see the new changes persisted on refresh
# Safari does not save + refresh properly through sauce labs
@skip_safari
Scenario: Settings are reset on cancel
Given I have set course dates
And I press the "Save" notification button
......@@ -41,6 +55,8 @@ Feature: Course Settings
And I press the "Cancel" notification button
Then I do not see the changes
# Safari gets CSRF token errors
@skip_safari
Scenario: Confirmation is shown on save
Given I have opened a new course in Studio
When I select Schedule and Details
......
......@@ -91,7 +91,7 @@ def remove_course_team_admin(_step, outer_capture, name):
@step(u'"([^"]*)" logs in$')
def other_user_login(_step, name):
world.browser.cookies.delete()
world.visit('logout')
world.visit('/')
signin_css = 'a.action-signin'
......
Feature: Course updates
As a course author, I want to be able to provide updates to my students
# Internet explorer can't select all so the update appears weirdly
@skip_internetexplorer
Scenario: Users can add updates
Given I have opened a new course in Studio
And I go to the course updates page
......@@ -8,6 +10,8 @@ Feature: Course updates
Then I should see the update "Hello"
And I see a "saving" notification
# Internet explorer can't select all so the update appears weirdly
@skip_internetexplorer
Scenario: Users can edit updates
Given I have opened a new course in Studio
And I go to the course updates page
......@@ -33,6 +37,8 @@ Feature: Course updates
Then I should see the date "June 1, 2013"
And I see a "saving" notification
# Internet explorer can't select all so the update appears weirdly
@skip_internetexplorer
Scenario: Users can change handouts
Given I have opened a new course in Studio
And I go to the course updates page
......
......@@ -6,6 +6,8 @@ Feature: Discussion Component Editor
And I edit and select Settings
Then I see three alphabetized settings and their expected values
# Safari doesn't save the name properly
@skip_safari
Scenario: User can modify display name
Given I have created a Discussion Tag
And I edit and select Settings
......
......@@ -13,7 +13,7 @@ Feature: Course Grading
When I add "6" new grades
Then I see I now have "5" grades
#Cannot reliably make the delete button appear so using javascript instead
# Cannot reliably make the delete button appear so using javascript instead
Scenario: Users can delete grading ranges
Given I have opened a new course in Studio
And I am viewing the grading settings
......@@ -21,6 +21,9 @@ Feature: Course Grading
And I delete a grade
Then I see I now have "2" grades
# IE and Safari cannot reliably drag and drop through selenium
@skip_internetexplorer
@skip_safari
Scenario: Users can move grading ranges
Given I have opened a new course in Studio
And I am viewing the grading settings
......@@ -85,6 +88,9 @@ Feature: Course Grading
When I change assignment type "Homework" to ""
Then the save button is disabled
# IE and Safari cannot type in grade range name
@skip_internetexplorer
@skip_safari
Scenario: User can edit grading range names
Given I have opened a new course in Studio
And I have populated the course
......
......@@ -112,10 +112,10 @@ def changes_not_persisted(step):
@step(u'I see the assignment type "(.*)"$')
def i_see_the_assignment_type(_step, name):
assignment_css = '#course-grading-assignment-name'
assignments = world.css_find(assignment_css)
types = [ele['value'] for ele in assignments]
assert name in types
assignment_css = '#course-grading-assignment-name'
assignments = world.css_find(assignment_css)
types = [ele['value'] for ele in assignments]
assert name in types
@step(u'I change the highest grade range to "(.*)"$')
......@@ -144,6 +144,7 @@ def cannot_edit_fail(_step):
pass # We should get this exception on failing to edit the element
@step(u'I change the grace period to "(.*)"$')
def i_change_grace_period(_step, grace_period):
grace_period_css = '#course-grading-graceperiod'
......
......@@ -6,6 +6,8 @@ Feature: HTML Editor
And I edit and select Settings
Then I see only the HTML display name setting
# Safari doesn't save the name properly
@skip_safari
Scenario: User can modify display name
Given I have created a Blank HTML Page
And I edit and select Settings
......
......@@ -7,12 +7,16 @@ Feature: Problem Editor
Then I see five alphabetized settings and their expected values
And Edit High Level Source is not visible
# Safari is having trouble saving the values on sauce
@skip_safari
Scenario: User can modify String values
Given I have created a Blank Common Problem
When I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
# Safari is having trouble saving the values on sauce
@skip_safari
Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem
When I edit and select Settings
......@@ -25,6 +29,8 @@ Feature: Problem Editor
Then I can revert the display name to unset
And my display name is unset on save
# IE will not click the revert button properly
@skip_internetexplorer
Scenario: User can select values in a Select
Given I have created a Blank Common Problem
When I edit and select Settings
......@@ -32,6 +38,8 @@ Feature: Problem Editor
And my change to randomization is persisted
And I can revert to the default value for randomization
# Safari will input it as 35.
@skip_safari
Scenario: User can modify float input values
Given I have created a Blank Common Problem
When I edit and select Settings
......@@ -44,16 +52,22 @@ Feature: Problem Editor
When I edit and select Settings
Then if I set the weight to "abc", it remains unset
# Safari will input it as 234.
@skip_safari
Scenario: User cannot type decimal values integer number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the max attempts to "2.34", it will persist as a valid integer
# Safari will input it incorrectly
@skip_safari
Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem
When I edit and select Settings
Then if I set the max attempts to "-3", it will persist as a valid integer
# Safari will input it as 35.
@skip_safari
Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem
When I edit and select Settings
......@@ -67,6 +81,8 @@ Feature: Problem Editor
Then Edit High Level Source is visible
# This feature will work in Firefox only when Firefox is the active window
# IE will not interact with the high level source in sauce labs
@skip_internetexplorer
Scenario: High Level source is persisted for LaTeX problem (bug STUD-280)
Given I have created a LaTeX Problem
When I edit and compile the High Level Source
......
......@@ -15,6 +15,8 @@ Feature: Static Pages
And I "delete" the "Empty" page
Then I should not see a "Empty" static page
# Safari won't update the name properly
@skip_safari
Scenario: Users can edit static pages
Given I have opened a new course in Studio
And I go to the static pages page
......
......@@ -25,6 +25,8 @@ Feature: Create Subsection
And I reload the page
Then I see it marked as Homework
# Safari has trouble saving the date in Sauce
@skip_safari
Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio
And I set the subsection release date to 12/25/2011 03:00
......
......@@ -5,6 +5,9 @@ Feature: Textbooks
When I go to the textbooks page
Then I should see a message telling me to create a new textbook
# IE and Safari on sauce labs will not upload the textbook correctly resulting in an error
@skip_internetexplorer
@skip_safari
Scenario: Create a textbook
Given I have opened a new course in Studio
And I go to the textbooks page
......
Feature: Upload Files
As a course author, I want to be able to upload files for my students
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can upload files
Given I have opened a new course in Studio
And I go to the files and uploads page
......@@ -8,6 +10,8 @@ Feature: Upload Files
Then I should see the file "test" was uploaded
And The url for the file "test" is valid
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can update files
Given I have opened a new course in studio
And I go to the files and uploads page
......@@ -15,6 +19,8 @@ Feature: Upload Files
And I upload the file "test"
Then I should see only one "test"
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can delete uploaded files
Given I have opened a new course in studio
And I go to the files and uploads page
......@@ -23,12 +29,16 @@ Feature: Upload Files
Then I should not see the file "test" was uploaded
And I see a confirmation that the file was deleted
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can download files
Given I have opened a new course in studio
And I go to the files and uploads page
When I upload the file "test"
Then I can download the correct "test" file
# Uploading isn't working on safari with sauce labs
@skip_safari
Scenario: Users can download updated files
Given I have opened a new course in studio
And I go to the files and uploads page
......
......@@ -10,6 +10,7 @@ import os
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
@step(u'I go to the files and uploads page')
def go_to_uploads(_step):
menu_css = 'li.nav-course-courseware'
......@@ -106,8 +107,8 @@ def get_index(file_name):
def get_file(file_name):
index = get_index(file_name)
assert index != -1
url_css = 'a.filename'
def get_url():
return world.css_find(url_css)[index]._element.get_attribute('href')
url = world.retry_on_exception(get_url)
......
......@@ -6,17 +6,23 @@ Feature: Video Component Editor
And I edit the component
Then I see the correct video settings and default values
# Safari has trouble saving values on Sauce
@skip_safari
Scenario: User can modify Video display name
Given I have created a Video component
And I edit the component
Then I can modify the display name
And my video display name change is persisted on save
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component
And I have set "show captions" to False
Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are shown when "show captions" is true
Given I have created a Video component
And I have set "show captions" to True
......
......@@ -10,15 +10,21 @@ Feature: Video Component
Given I have clicked the new unit button
Then creating a video takes a single click
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden correctly
Given I have created a Video component
And I have hidden captions
Then when I view the video it does not show the captions
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are shown correctly
Given I have created a Video component
Then when I view the video it does show the captions
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are toggled correctly
Given I have created a Video component
And I have toggled captions
......
......@@ -2,7 +2,7 @@
Script for importing courseware from XML format
"""
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand, CommandError, make_option
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
......@@ -14,18 +14,26 @@ class Command(BaseCommand):
"""
help = 'Import the specified data directory into the default ModuleStore'
option_list = BaseCommand.option_list + (
make_option('--nostatic',
action='store_true',
help='Skip import of static content'),
)
def handle(self, *args, **options):
"Execute the command"
if len(args) == 0:
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
raise CommandError("import requires at least one argument: <data directory> [--nostatic] [<course dir>...]")
data_dir = args[0]
do_import_static = not (options.get('nostatic', False))
if len(args) > 1:
course_dirs = args[1:]
else:
course_dirs = None
print("Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir,
courses=course_dirs))
courses=course_dirs,
dis=do_import_static))
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False,
static_content_store=contentstore(), verbose=True)
static_content_store=contentstore(), verbose=True, do_import_static=do_import_static)
......@@ -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)
......
......@@ -486,7 +486,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
content_store = contentstore()
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, verbose=True)
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
course = module_store.get_item(course_location)
......@@ -962,8 +962,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'vertical', 'vertical_test', None]), depth=1)
self.assertTrue(getattr(vertical, 'is_draft', False))
self.assertNotIn('index_in_children_list', child.xml_attributes)
self.assertNotIn('parent_sequential_url', vertical.xml_attributes)
for child in vertical.get_children():
self.assertTrue(getattr(child, 'is_draft', False))
self.assertNotIn('index_in_children_list', child.xml_attributes)
if hasattr(child, 'data'):
self.assertNotIn('index_in_children_list', child.data)
self.assertNotIn('parent_sequential_url', child.xml_attributes)
if hasattr(child, 'data'):
self.assertNotIn('parent_sequential_url', child.data)
# make sure that we don't have a sequential that is in draft mode
sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy',
......@@ -1074,6 +1083,38 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# It should now contain empty data
self.assertEquals(imported_word_cloud.data, '')
def test_html_export_roundtrip(self):
"""
Test that a course which has HTML that has style formatting is preserved in export/import
"""
module_store = modulestore('direct')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['toy'])
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
# Export the course
root_dir = path(mkdtemp_clean())
export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip')
# Reimport and get the video back
import_from_xml(module_store, root_dir)
# get the sample HTML with styling information
html_module = module_store.get_instance(
'edX/toy/2012_Fall',
Location(['i4x', 'edX', 'toy', 'html', 'with_styling'])
)
self.assertIn('<p style="font:italic bold 72px/30px Georgia, serif; color: red; ">', html_module.data)
# get the sample HTML with just a simple <img> tag information
html_module = module_store.get_instance(
'edX/toy/2012_Fall',
Location(['i4x', 'edX', 'toy', 'html', 'just_img'])
)
self.assertIn('<img src="/static/foo_bar.jpg" />', html_module.data)
def test_course_handouts_rewrites(self):
module_store = modulestore('direct')
......@@ -1384,7 +1425,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
......@@ -1394,92 +1435,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')
......
......@@ -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)
#pylint: disable=E1101
'''
Tests for importing with no static
'''
from django.test.client import Client
from django.test.utils import override_settings
from django.conf import settings
from path import path
import copy
from django.contrib.auth.models import User
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
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)
class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
"""
Tests that rely on the toy and test_import_course courses.
NOTE: refactor using CourseFactory so they do not.
"""
def setUp(self):
settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
# Create the use so we can log them in.
self.user = User.objects.create_user(uname, email, password)
# Note that we do not actually need to do anything
# for registration if we directly mark them active.
self.user.is_active = True
# Staff has access to view all courses
self.user.is_staff = True
# Save the data that we've just changed to the db.
self.user.save()
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).
'''
content_store = contentstore()
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['test_import_course'], static_content_store=content_store, do_import_static=False, verbose=True)
course_location = CourseDescriptor.id_to_location('edX/test_import_course/2012_Fall')
course = module_store.get_item(course_location)
self.assertIsNotNone(course)
return module_store, content_store, course, course_location
def test_static_import(self):
'''
Stuff in static_import should always be imported into contentstore
'''
_, content_store, course, course_location = self.load_test_import_course()
# make sure we have ONE asset in our contentstore ("should_be_imported.html")
all_assets = content_store.get_all_content_for_course(course_location)
print "len(all_assets)=%d" % len(all_assets)
self.assertEqual(len(all_assets), 1)
content = None
try:
location = StaticContent.get_location_from_path('/c4x/edX/test_import_course/asset/should_be_imported.html')
content = content_store.find(location)
except NotFoundError:
pass
self.assertIsNotNone(content)
# make sure course.lms.static_asset_path is correct
print "static_asset_path = {0}".format(course.lms.static_asset_path)
self.assertEqual(course.lms.static_asset_path, 'test_import_course')
def test_asset_import_nostatic(self):
'''
This test validates that an image asset is NOT imported when do_import_static=False
'''
content_store = contentstore()
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True)
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
module_store.get_item(course_location)
# make sure we have NO assets in our contentstore
all_assets = content_store.get_all_content_for_course(course_location)
print "len(all_assets)=%d" % len(all_assets)
self.assertEqual(len(all_assets), 0)
def test_no_static_link_rewrites_on_import(self):
module_store = modulestore('direct')
import_from_xml(module_store, 'common/test/data/', ['toy'], do_import_static=False, verbose=True)
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course_info', 'handouts', None]))
self.assertIn('/static/', handouts.data)
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
self.assertIn('/static/', handouts.data)
......@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
resp.content,
"application/json"
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
class TestCreateItem(CourseTestCase):
......
......@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase):
def test_view_index(self):
"Basic check that the textbook index page responds correctly"
resp = self.client.get(self.url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# we don't have resp.context right now,
# due to bugs in our testing harness :(
if resp.context:
......@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content)
self.assertEqual(self.course.pdf_textbooks, obj)
......@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content)
self.assertEqual(content, obj)
......@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
# reload course
store = get_modulestore(self.course.location)
......@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
obj = json.loads(resp.content)
self.assertIn("error", obj)
......@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase):
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
self.assertNotIn("Location", resp)
......@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase):
def test_get_1(self):
"Get the first textbook"
resp = self.client.get(self.url1)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook1)
def test_get_2(self):
"Get the second textbook"
resp = self.client.get(self.url2)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook2)
......@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase):
def test_delete(self):
"Delete a textbook by ID"
resp = self.client.delete(self.url1)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
course = self.store.get_item(self.course.location)
self.assertEqual(course.pdf_textbooks, [self.textbook2])
......@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase):
)
self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(url)
self.assert2XX(resp2.status_code)
self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content)
self.assertEqual(compare, textbook)
course = self.store.get_item(self.course.location)
......@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase):
)
self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(self.url2)
self.assert2XX(resp2.status_code)
self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content)
self.assertEqual(compare, replacement)
course = self.store.get_item(self.course.location)
......
......@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase):
def test_detail_inactive(self):
resp = self.client.get(self.inactive_detail_url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 200)
result = json.loads(resp.content)
self.assertFalse(result["active"])
def test_detail_invalid(self):
resp = self.client.get(self.invalid_detail_url)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 404)
result = json.loads(resp.content)
self.assertIn("error", result)
......@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
data={"role": None},
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
self.assert_not_enrolled()
......@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
self.assert_not_enrolled()
......@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "staff"},
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()]
......@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"},
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
......@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"},
HTTP_ACCEPT="application/json",
)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
......@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase):
})
resp = self.client.delete(self_url)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
# reload user from DB
user = User.objects.get(email=self.user.email)
groups = [g.name for g in user.groups.all()]
......@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase):
self.ext_user.save()
resp = self.client.delete(self.detail_url)
self.assert4XX(resp.status_code)
self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content)
self.assertIn("error", result)
# reload user from DB
......@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url,
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
self.assert_enrolled()
def test_staff_to_instructor_still_enrolled(self):
......@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json",
HTTP_ACCEPT="application/json",
)
self.assert2XX(resp.status_code)
self.assertEqual(resp.status_code, 204)
self.assert_enrolled()
def assert_not_enrolled(self):
......
......@@ -8,6 +8,7 @@ so that we can run the lettuce acceptance tests.
# pylint: disable=W0401, W0614
from .test import *
from lms.envs.sauce import *
# You need to start the server in debug mode,
# otherwise the browser will not render the pages correctly
......@@ -17,7 +18,7 @@ DEBUG = True
import logging
logging.disable(logging.ERROR)
import os
import random
from random import choice, randint
def seed():
......@@ -75,7 +76,6 @@ DATABASES = {
# Use the auto_auth workflow for creating users and logging them in
MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# HACK
# Setting this flag to false causes imports to not load correctly in the lettuce python files
# We do not yet understand why this occurs. Setting this to true is a stopgap measure
......@@ -84,5 +84,5 @@ USE_I18N = True
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = random.randint(1024, 65535)
LETTUCE_BROWSER = 'chrome'
LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome')
......@@ -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):
"""
......
......@@ -33,6 +33,7 @@ class CourseMode(models.Model):
currency = models.CharField(default="usd", max_length=8)
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd')
DEFAULT_MODE_SLUG = 'honor'
class Meta:
""" meta attributes of this model """
......@@ -51,3 +52,18 @@ class CourseMode(models.Model):
if not modes:
modes = [cls.DEFAULT_MODE]
return modes
@classmethod
def mode_for_course(cls, course_id, mode_slug):
"""
Returns the mode for the course corresponding to mode_slug.
If this particular mode is not set for the course, returns None
"""
modes = cls.modes_for_course(course_id)
matched = [m for m in modes if m.slug == mode_slug]
if matched:
return matched[0]
else:
return None
......@@ -60,3 +60,6 @@ class CourseModeModelTest(TestCase):
modes = CourseMode.modes_for_course(self.course_id)
self.assertEqual(modes, set_modes)
self.assertEqual(mode1, CourseMode.mode_for_course(self.course_id, u'honor'))
self.assertEqual(mode2, CourseMode.mode_for_course(self.course_id, u'verified'))
self.assertIsNone(CourseMode.mode_for_course(self.course_id, 'DNE'))
......@@ -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):
......
......@@ -90,7 +90,7 @@ def replace_course_urls(text, course_id):
return re.sub(_url_replace_regex('/course/'), replace_course_url, text)
def replace_static_urls(text, data_directory, course_id=None):
def replace_static_urls(text, data_directory, course_id=None, static_asset_path=''):
"""
Replace /static/$stuff urls either with their correct url as generated by collectstatic,
(/static/$md5_hashed_stuff) or by the course-specific content static url
......@@ -100,6 +100,7 @@ def replace_static_urls(text, data_directory, course_id=None):
text: The source text to do the substitution in
data_directory: The directory in which course data is stored
course_id: The course identifier used to distinguish static content for this course in studio
static_asset_path: Path for static assets, which overrides data_directory and course_namespace, if nonempty
"""
def replace_static_url(match):
......@@ -116,7 +117,7 @@ def replace_static_urls(text, data_directory, course_id=None):
if settings.DEBUG and finders.find(rest, True):
return original
# if we're running with a MongoBacked store course_namespace is not None, then use studio style urls
elif course_id and modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE:
elif (not static_asset_path) and course_id and modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE:
# first look in the static file pipeline and see if we are trying to reference
# a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule)
......@@ -135,7 +136,7 @@ def replace_static_urls(text, data_directory, course_id=None):
url = StaticContent.convert_legacy_static_url_with_course_id(rest, course_id)
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
else:
course_path = "/".join((data_directory, rest))
course_path = "/".join((static_asset_path or data_directory, rest))
try:
if staticfiles_storage.exists(rest):
......@@ -152,7 +153,7 @@ def replace_static_urls(text, data_directory, course_id=None):
return re.sub(
_url_replace_regex('/static/(?!{data_dir})'.format(data_dir=data_directory)),
_url_replace_regex('/static/(?!{data_dir})'.format(data_dir=static_asset_path or data_directory)),
replace_static_url,
text
)
......@@ -827,9 +827,6 @@ class CourseEnrollment(models.Model):
@classmethod
def is_enrolled(cls, user, course_id):
"""
Remove the user from a given course. If the relevant `CourseEnrollment`
object doesn't exist, we log an error but don't throw an exception.
Returns True if the user is enrolled in the course (the entry must exist
and it must have `is_active=True`). Otherwise, returns False.
......
......@@ -11,6 +11,10 @@ from logging import getLogger
from django.core.management import call_command
from django.conf import settings
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
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
......@@ -42,43 +46,93 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...")
MAX_VALID_BROWSER_ATTEMPTS = 20
def get_username_and_key():
"""
Returns the Sauce Labs username and access ID as set by environment variables
"""
return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')}
def set_job_status(jobid, passed=True):
"""
Sets the job status on sauce labs
"""
body_content = dumps({"passed": passed})
config = get_username_and_key()
base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1]
result = put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid),
data=body_content,
headers={"Authorization": "Basic {}".format(base64string)})
return result.status_code == 200
def make_desired_capabilities():
"""
Returns a DesiredCapabilities object corresponding to the environment sauce parameters
"""
desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME)
desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM')
desired_capabilities['version'] = settings.SAUCE.get('VERSION')
desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE')
desired_capabilities['name'] = settings.SAUCE.get('SESSION')
desired_capabilities['build'] = settings.SAUCE.get('BUILD')
desired_capabilities['video-upload-on-pass'] = False
desired_capabilities['sauce-advisor'] = False
desired_capabilities['record-screenshots'] = False
desired_capabilities['selenium-version'] = "2.34.0"
desired_capabilities['max-duration'] = 3600
desired_capabilities['public'] = 'public restricted'
return desired_capabilities
@before.harvest
def initial_setup(server):
"""
Launch the browser once before executing the tests.
"""
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
world.absorb(settings.SAUCE.get('SAUCE_ENABLED'), 'SAUCE_ENABLED')
# There is an issue with ChromeDriver2 r195627 on Ubuntu
# in which we sometimes get an invalid browser session.
# This is a work-around to ensure that we get a valid session.
success = False
num_attempts = 0
while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
if not world.SAUCE_ENABLED:
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
# Get a browser session
world.browser = Browser(browser_driver)
# There is an issue with ChromeDriver2 r195627 on Ubuntu
# in which we sometimes get an invalid browser session.
# This is a work-around to ensure that we get a valid session.
success = False
num_attempts = 0
while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
world.browser = Browser(browser_driver)
# Try to visit the main page
# If the browser session is invalid, this will
# raise a WebDriverException
try:
world.visit('/')
# Try to visit the main page
# If the browser session is invalid, this will
# raise a WebDriverException
try:
world.visit('/')
except WebDriverException:
world.browser.quit()
num_attempts += 1
except WebDriverException:
world.browser.quit()
num_attempts += 1
else:
success = True
else:
success = True
# If we were unable to get a valid session within the limit of attempts,
# then we cannot run the tests.
if not success:
raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
# If we were unable to get a valid session within the limit of attempts,
# then we cannot run the tests.
if not success:
raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
# Set the browser size to 1280x1024
world.browser.driver.set_window_size(1280, 1024)
world.browser.driver.set_window_size(1280, 1024)
else:
config = get_username_and_key()
world.browser = Browser(
'remote',
url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']),
**make_desired_capabilities()
)
world.browser.driver.implicitly_wait(30)
world.absorb(world.browser.driver.session_id, 'jobid')
@before.each_scenario
......@@ -97,7 +151,6 @@ def clear_data(scenario):
world.spew('scenario_dict')
@after.each_scenario
def reset_databases(scenario):
'''
......@@ -108,9 +161,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
......@@ -128,4 +182,6 @@ def teardown_browser(total):
"""
Quit the browser after executing the tests.
"""
if world.SAUCE_ENABLED:
set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed)
world.browser.quit()
......@@ -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()
......@@ -99,7 +99,7 @@ def i_am_logged_in_user(step):
@step('I am not logged in$')
def i_am_not_logged_in(step):
world.browser.cookies.delete()
world.visit('logout')
@step('I am staff for course "([^"]*)"$')
......@@ -138,10 +138,13 @@ def should_have_link_with_path_and_text(step, path, text):
@step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
def should_see_in_the_page(step, doesnt_appear, text):
multiplier = 1
if world.SAUCE_ENABLED:
multiplier = 2
if doesnt_appear:
assert world.browser.is_text_not_present(text, wait_time=5)
assert world.browser.is_text_not_present(text, wait_time=5*multiplier)
else:
assert world.browser.is_text_present(text, wait_time=5)
assert world.browser.is_text_present(text, wait_time=5*multiplier)
@step('I am logged in$')
......@@ -150,7 +153,7 @@ def i_am_logged_in(step):
world.log_in(username='robot', password='test')
world.browser.visit(django_url('/'))
# You should not see the login link
assert_equals(world.browser.find_by_css('a#login'), [])
assert world.is_css_not_present('a#login')
@step(u'I am an edX user$')
......
......@@ -76,7 +76,7 @@ def replace_course_urls(get_html, course_id):
return _get_html
def replace_static_urls(get_html, data_dir, course_id=None):
def replace_static_urls(get_html, data_dir, course_id=None, static_asset_path=''):
"""
Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/...
......@@ -85,7 +85,7 @@ def replace_static_urls(get_html, data_dir, course_id=None):
@wraps(get_html)
def _get_html():
return static_replace.replace_static_urls(get_html(), data_dir, course_id)
return static_replace.replace_static_urls(get_html(), data_dir, course_id, static_asset_path=static_asset_path)
return _get_html
......
......@@ -213,7 +213,7 @@ class CombinedOpenEndedFields(object):
help="The number of times the student can try to answer this problem.",
default=1,
scope=Scope.settings,
values={"min" : 1 }
values={"min": 1 }
)
accept_file_upload = Boolean(
display_name="Allow File Uploads",
......@@ -229,12 +229,10 @@ class CombinedOpenEndedFields(object):
)
due = Date(
help="Date that this problem is due by",
default=None,
scope=Scope.settings
)
graceperiod = Timedelta(
help="Amount of time after the due date that submissions will be accepted",
default=None,
scope=Scope.settings
)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
......@@ -244,7 +242,7 @@ class CombinedOpenEndedFields(object):
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings,
values={"min" : 0 , "step": ".1"},
values={"min": 0, "step": ".1"},
default=1
)
min_to_calibrate = Integer(
......@@ -252,28 +250,28 @@ class CombinedOpenEndedFields(object):
help="The minimum number of calibration essays each student will need to complete for peer grading.",
default=3,
scope=Scope.settings,
values={"min" : 1, "max" : 20, "step" : "1"}
values={"min": 1, "max": 20, "step": "1"}
)
max_to_calibrate = Integer(
display_name="Maximum Peer Grading Calibrations",
help="The maximum number of calibration essays each student will need to complete for peer grading.",
default=6,
scope=Scope.settings,
values={"min" : 1, "max" : 20, "step" : "1"}
values={"min": 1, "max": 20, "step": "1"}
)
peer_grader_count = Integer(
display_name="Peer Graders per Response",
help="The number of peers who will grade each submission.",
default=3,
scope=Scope.settings,
values={"min" : 1, "step" : "1", "max" : 5}
values={"min": 1, "step": "1", "max": 5}
)
required_peer_grading = Integer(
display_name="Required Peer Grading",
help="The number of other students each student making a submission will have to grade.",
default=3,
scope=Scope.settings,
values={"min" : 1, "step" : "1", "max" : 5}
values={"min": 1, "step": "1", "max": 5}
)
markdown = String(
help="Markdown source of this module",
......
......@@ -33,11 +33,13 @@ class HtmlFields(object):
class HtmlModule(HtmlFields, XModule):
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee')
]
}
js = {
'coffee': [
resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee')
]
}
js_module_name = "HTMLModule"
css = {'scss': [resource_string(__name__, 'css/html/display.scss')]}
......@@ -118,8 +120,10 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
# from .html
# 'filename' in html pointers is a relative path
# (not same as 'html/blah.html' when the pointer is in a directory itself)
pointer_path = "{category}/{url_path}".format(category='html',
url_path=name_to_pathname(location.name))
pointer_path = "{category}/{url_path}".format(
category='html',
url_path=name_to_pathname(location.name)
)
base = path(pointer_path).dirname()
# log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
filepath = "{base}/{name}.html".format(base=base, name=filename)
......@@ -164,19 +168,16 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
# TODO (vshnayder): make export put things in the right places.
def definition_to_xml(self, resource_fs):
'''If the contents are valid xml, write them to filename.xml. Otherwise,
write just <html filename="" [meta-attrs="..."]> to filename.xml, and the html
''' Write <html filename="" [meta-attrs="..."]> to filename.xml, and the html
string to filename.html.
'''
try:
return etree.fromstring(self.data)
except etree.XMLSyntaxError:
pass
# Not proper format. Write html to file, return an empty tag
# Write html to file, return an empty tag
pathname = name_to_pathname(self.url_name)
filepath = u'{category}/{pathname}.html'.format(category=self.category,
pathname=pathname)
filepath = u'{category}/{pathname}.html'.format(
category=self.category,
pathname=pathname
)
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as filestream:
......@@ -190,6 +191,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
elt.set("filename", relname)
return elt
class AboutFields(object):
display_name = String(
help="Display name for this module",
......@@ -202,12 +204,14 @@ class AboutFields(object):
scope=Scope.content
)
class AboutModule(AboutFields, HtmlModule):
"""
Overriding defaults but otherwise treated as HtmlModule.
"""
pass
class AboutDescriptor(AboutFields, HtmlDescriptor):
"""
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
......@@ -216,6 +220,7 @@ class AboutDescriptor(AboutFields, HtmlDescriptor):
template_dir_name = "about"
module_class = AboutModule
class StaticTabFields(object):
"""
The overrides for Static Tabs
......@@ -241,6 +246,7 @@ class StaticTabModule(StaticTabFields, HtmlModule):
"""
pass
class StaticTabDescriptor(StaticTabFields, HtmlDescriptor):
"""
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
......
<section class="course-content">
<section class="xmodule_display xmodule_CombinedOpenEndedModule" data-type="CombinedOpenEnded">
<section class="xmodule_display xmodule_CombinedOpenEndedModule" data-type="CombinedOpenEnded">
<section id="combined-open-ended" class="combined-open-ended" data-ajax-url="/courses/MITx/6.002x/2012_Fall/modx/i4x://MITx/6.002x/combinedopenended/CombinedOE" data-allow_reset="False" data-state="assessing" data-task-count="2" data-task-number="1">
<h2>Problem 1</h2>
<div class="status-container">
<h4>Status</h4>
<div class="status-elements">
<section id="combined-open-ended-status" class="combined-open-ended-status">
<div class="statusitem" data-status-number="0">
Step 1 (Problem complete) : 1 / 1
<h2>Problem 1</h2>
<div class="status-container">
<h4>Status</h4>
<div class="status-elements">
<section id="combined-open-ended-status" class="combined-open-ended-status">
<div class="statusitem" data-status-number="0">
Step 1 (Problem complete) : 1 / 1
<span class="correct" id="status"></span>
</div>
<div class="statusitem statusitem-current" data-status-number="1">
Step 2 (Being scored) : None / 1
</div>
<div class="statusitem statusitem-current" data-status-number="1">
Step 2 (Being scored) : None / 1
<span class="grading" id="status"></span>
</div>
</section>
</div>
</div>
</section>
</div>
</div>
<div class="item-container">
<h4>Problem</h4>
<div class="item-container">
<h4>Problem</h4>
<div class="problem-container">
<div class="item"><section id="openended_open_ended" class="open-ended-child" data-state="assessing" data-child-type="openended"><div class="error"></div>
<div class="prompt">
Some prompt.
</div>
<textarea rows="30" cols="80" name="answer" class="answer short-form-response" id="input_open_ended" disabled="disabled">Test submission. Yaaaaaaay!</textarea><div class="message-wrapper"></div>
<div class="grader-status">
<span class="grading" id="status_open_ended">Submitted for grading.</span>
</div>
<input type="button" value="Submit assessment" class="submit-button" name="show" style="display: none;"><input name="skip" class="skip-button" type="button" value="Skip Post-Assessment" style="display: none;"><div class="open-ended-action"></div>
<span id="answer_open_ended"></span>
</section></div>
<div class="item">
<section id="openended_open_ended" class="open-ended-child" data-state="assessing" data-child-type="openended">
<div class="error">
</div>
<div class="prompt">
Some prompt.
</div>
<textarea rows="30" cols="80" name="answer" class="answer short-form-response" id="input_open_ended" disabled="disabled">
Test submission. Yaaaaaaay!
</textarea>
<div class="message-wrapper"></div>
<div class="grader-status">
<span class="grading" id="status_open_ended">Submitted for grading.</span>
</div>
<input type="button" value="Submit assessment" class="submit-button" name="show" style="display: none;">
<input name="skip" class="skip-button" type="button" value="Skip Post-Assessment" style="display: none;">
<div class="open-ended-action"></div>
<span id="answer_open_ended"></span>
</section>
</div>
</div>
<div class="oe-tools response-tools">
<span class="oe-tools-label"></span>
<input type="button" value="Reset" class="reset-button" name="reset" style="display: none;">
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
</div>
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
</div>
<a name="results">
<div class="result-container">
</div>
</a></section><a name="results">
</a></section><a name="results">
</a><div><a name="results">
</a><a href="https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml">Edit</a> /
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" onclick="javascript:getlog('i4x_MITx_6_002x_combinedopenended_CombinedOE', {
<div class="result-container">
</div>
</a>
</section>
<a name="results">
</a>
</section>
<a name="results">
</a>
<div>
<a name="results">
</a>
<a href="https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml">
Edit
</a> /
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" onclick="javascript:getlog('i4x_MITx_6_002x_combinedopenended_CombinedOE', {
'location': 'i4x://MITx/6.002x/combinedopenended/CombinedOE',
'xqa_key': 'KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A',
'category': 'CombinedOpenEndedModule',
'user': 'blah'
})" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA</a>
</div>
<div><a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_debug" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_trig">Staff Debug Info</a></div>
</div>
<div>
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_debug" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_trig">
Staff Debug Info
</a>
</div>
<section id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto">
<div class="inner-wrapper">
......
describe 'Rubric', ->
beforeEach ->
spyOn Logger, 'log'
# load up some fixtures
loadFixtures 'rubric.html'
jasmine.Clock.useMock()
@element = $('.combined-open-ended')
@location = @element.data('location')
describe 'constructor', ->
beforeEach ->
@rub = new Rubric @element
it 'rubric should properly grab the element', ->
expect(@rub.el).toEqual @element
describe 'initialize', ->
beforeEach ->
@rub = new Rubric @element
@rub.initialize @location
it 'rubric correctly sets location', ->
expect($(@rub.rubric_sel).data('location')).toEqual @location
it 'rubric correctly read', ->
expect(@rub.categories.length).toEqual 5
describe 'CombinedOpenEnded', ->
beforeEach ->
spyOn Logger, 'log'
......@@ -13,7 +40,7 @@ describe 'CombinedOpenEnded', ->
@combined = new CombinedOpenEnded @element
it 'set the element', ->
expect(@combined.element).toEqual @element
expect(@combined.el).toEqual @element
it 'get the correct values from data fields', ->
expect(@combined.ajax_url).toEqual '/courses/MITx/6.002x/2012_Fall/modx/i4x://MITx/6.002x/combinedopenended/CombinedOE'
......@@ -77,7 +104,7 @@ describe 'CombinedOpenEnded', ->
@combined.child_state = 'done'
@combined.rebind()
expect(@combined.answer_area.attr("disabled")).toBe("disabled")
expect(@combined.next_problem).toHaveBeenCalled()
expect(@combined.next_problem_button).toBe(":visible")
describe 'next_problem', ->
beforeEach ->
......@@ -109,3 +136,5 @@ describe 'CombinedOpenEnded', ->
......@@ -3,10 +3,20 @@
# Can (and should be) expanded upon when our problem list
# becomes more sophisticated
class @PeerGrading
peer_grading_sel: '.peer-grading'
peer_grading_container_sel: '.peer-grading-container'
error_container_sel: '.error-container'
message_container_sel: '.message-container'
problem_button_sel: '.problem-button'
problem_list_sel: '.problem-list'
progress_bar_sel: '.progress-bar'
constructor: (element) ->
@peer_grading_container = $('.peer-grading')
@el = element
@peer_grading_container = @$(@peer_grading_sel)
@use_single_location = @peer_grading_container.data('use-single-location')
@peer_grading_outer_container = $('.peer-grading-container')
@peer_grading_outer_container = @$(@peer_grading_container_sel)
@ajax_url = @peer_grading_container.data('ajax-url')
if @use_single_location.toLowerCase() == "true"
......@@ -14,23 +24,27 @@ class @PeerGrading
@activate_problem()
else
#Otherwise, activate the panel view.
@error_container = $('.error-container')
@error_container = @$(@error_container_sel)
@error_container.toggle(not @error_container.is(':empty'))
@message_container = $('.message-container')
@message_container = @$(@message_container_sel)
@message_container.toggle(not @message_container.is(':empty'))
@problem_button = $('.problem-button')
@problem_button = @$(@problem_button_sel)
@problem_button.click @show_results
@problem_list = $('.problem-list')
@problem_list = @$(@problem_list_sel)
@construct_progress_bar()
# locally scoped jquery.
$: (selector) ->
$(selector, @el)
construct_progress_bar: () =>
problems = @problem_list.find('tr').next()
problems.each( (index, element) =>
problem = $(element)
progress_bar = problem.find('.progress-bar')
progress_bar = problem.find(@progress_bar_sel)
bar_value = parseInt(problem.data('graded'))
bar_max = parseInt(problem.data('required')) + bar_value
progress_bar.progressbar({value: bar_value, max: bar_max})
......@@ -43,10 +57,10 @@ class @PeerGrading
if response.success
@peer_grading_outer_container.after(response.html).remove()
backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend)
new PeerGradingProblem(backend, @el)
else
@gentle_alert response.error
activate_problem: () =>
backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend)
\ No newline at end of file
new PeerGradingProblem(backend, @el)
\ No newline at end of file
......@@ -55,6 +55,7 @@ def modulestore(name='default'):
return _MODULESTORES[name]
_loc_singleton = None
def loc_mapper():
"""
......@@ -69,3 +70,42 @@ def loc_mapper():
_loc_singleton = LocMapperStore(settings.modulestore_options)
return _loc_singleton
def clear_existing_modulestores():
"""
Clear the existing modulestore instances, causing
them to be re-created when accessed again.
This is useful for flushing state between unit tests.
"""
_MODULESTORES.clear()
def editable_modulestore(name='default'):
"""
Retrieve a modulestore that we can modify.
This is useful for tests that need to insert test
data into the modulestore.
Currently, only Mongo-backed modulestores can be modified.
Returns `None` if no editable modulestore is available.
"""
# Try to retrieve the ModuleStore
# Depending on the settings, this may or may not
# be editable.
store = modulestore(name)
# If this is a `MixedModuleStore`, then we will need
# to retrieve the actual Mongo instance.
# We assume that the default is Mongo.
if hasattr(store, 'modulestores'):
store = store.modulestores['default']
# At this point, we either have the ability to create
# items in the store, or we do not.
if hasattr(store, 'create_xmodule'):
return store
else:
return None
......@@ -9,7 +9,8 @@ INHERITABLE_METADATA = (
# intended to be set per-course, but can be overridden in for specific
# elements. Can be a float.
'days_early_for_beta',
'giturl' # for git edit link
'giturl', # for git edit link
'static_asset_path', # for static assets placed outside xcontent contentstore
)
......
"""
Modulestore configuration for test cases.
"""
import copy
from uuid import uuid4
from django.test import TestCase
from xmodule.modulestore.django import editable_modulestore, \
clear_existing_modulestores
from django.conf import settings
import xmodule.modulestore.django
from unittest.util import safe_repr
def mixed_store_config(data_dir, mappings):
"""
Return a `MixedModuleStore` configuration, which provides
access to both Mongo- and XML-backed courses.
`data_dir` is the directory from which to load XML-backed courses.
`mappings` is a dictionary mapping course IDs to modulestores, for example:
{
'MITx/2.01x/2013_Spring': 'xml',
'edx/999/2013_Spring': 'default'
}
where 'xml' and 'default' are the two options provided by this configuration,
mapping (respectively) to XML-backed and Mongo-backed modulestores..
"""
mongo_config = mongo_store_config(data_dir)
xml_config = xml_store_config(data_dir)
store = {
'default': {
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': {
'mappings': mappings,
'stores': {
'default': mongo_config['default'],
'xml': xml_config['default']
}
}
}
}
store['direct'] = store['default']
return store
def mongo_store_config(data_dir):
......@@ -27,6 +62,7 @@ def mongo_store_config(data_dir):
}
}
}
store['direct'] = store['default']
return store
......@@ -45,23 +81,22 @@ def draft_mongo_store_config(data_dir):
'render_template': 'mitxmako.shortcuts.render_to_string'
}
return {
store = {
'default': {
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
}
}
store['direct'] = store['default']
return store
def xml_store_config(data_dir):
"""
Defines default module store using XMLModuleStore.
"""
return {
store = {
'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': {
......@@ -71,12 +106,48 @@ def xml_store_config(data_dir):
}
}
store['direct'] = store['default']
return store
class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb
module store. This populates a uniquely named modulestore
collection with templates before running the TestCase
and drops it they are finished. """
"""
Subclass for any test case that uses a ModuleStore.
Ensures that the ModuleStore is cleaned before/after each test.
Usage:
1. Create a subclass of `ModuleStoreTestCase`
2. Use Django's @override_settings decorator to use
the desired modulestore configuration.
For example:
MIXED_CONFIG = mixed_store_config(data_dir, mappings)
@override_settings(MODULESTORE=MIXED_CONFIG)
class FooTest(ModuleStoreTestCase):
# ...
3. Use factories (e.g. `CourseFactory`, `ItemFactory`) to populate
the modulestore with test data.
NOTE:
* For Mongo-backed courses (created with `CourseFactory`),
the state of the course will be reset before/after each
test method executes.
* For XML-backed courses, the course state will NOT
reset between test methods (although it will reset
between test classes)
The reason is: XML courses are not editable, so to reset
a course you have to reload it from disk, which is slow.
If you do need to reset an XML course, use
`clear_existing_modulestores()` directly in
your `setUp()` method.
"""
@staticmethod
def update_course(course, data):
......@@ -89,107 +160,68 @@ class ModuleStoreTestCase(TestCase):
'data' is a dictionary with an entry for each CourseField we want to update.
"""
store = xmodule.modulestore.django.modulestore()
store = editable_modulestore('direct')
store.update_metadata(course.location, data)
updated_course = store.get_instance(course.id, course.location)
return updated_course
@staticmethod
def flush_mongo_except_templates():
def drop_mongo_collection():
"""
Delete everything in the module store except templates.
If using a Mongo-backed modulestore, drop the collection.
"""
modulestore = xmodule.modulestore.django.modulestore()
# This query means: every item in the collection
# that is not a template
query = {"_id.course": {"$ne": "templates"}}
# This will return the mongo-backed modulestore
# even if we're using a mixed modulestore
store = editable_modulestore()
# Remove everything except templates
modulestore.collection.remove(query)
modulestore.collection.drop()
if hasattr(store, 'collection'):
store.collection.drop()
@classmethod
def setUpClass(cls):
"""
Flush the mongo store and set up templates.
Delete the existing modulestores, causing them to be reloaded.
"""
# Use a uuid to differentiate
# the mongo collections on jenkins.
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
if 'direct' not in settings.MODULESTORE:
settings.MODULESTORE['direct'] = settings.MODULESTORE['default']
settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
xmodule.modulestore.django._MODULESTORES.clear()
print settings.MODULESTORE
# Clear out any existing modulestores,
# which will cause them to be re-created
# the next time they are accessed.
clear_existing_modulestores()
TestCase.setUpClass()
@classmethod
def tearDownClass(cls):
"""
Revert to the old modulestore settings.
Drop the existing modulestores, causing them to be reloaded.
Clean up any data stored in Mongo.
"""
# Clean up by flushing the Mongo modulestore
cls.drop_mongo_collection()
# Clean up by dropping the collection
modulestore = xmodule.modulestore.django.modulestore()
modulestore.collection.drop()
# Clear out the existing modulestores,
# which will cause them to be re-created
# the next time they are accessed.
# We do this at *both* setup and teardown just to be safe.
clear_existing_modulestores()
xmodule.modulestore.django._MODULESTORES.clear()
# Restore the original modulestore settings
settings.MODULESTORE = cls.orig_modulestore
TestCase.tearDownClass()
def _pre_setup(self):
"""
Remove everything but the templates before each test.
Flush the ModuleStore before each test.
"""
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
# Flush the Mongo modulestore
ModuleStoreTestCase.drop_mongo_collection()
# Call superclass implementation
super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self):
"""
Flush everything we created except the templates.
Flush the ModuleStore after each test.
"""
# Flush anything that is not a template
ModuleStoreTestCase.flush_mongo_except_templates()
ModuleStoreTestCase.drop_mongo_collection()
# Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown()
def assert2XX(self, status_code, msg=None):
"""
Assert that the given value is a success status (between 200 and 299)
"""
msg = self._formatMessage(msg, "%s is not a success status" % safe_repr(status_code))
self.assertTrue(status_code >= 200 and status_code < 300, msg=msg)
def assert3XX(self, status_code, msg=None):
"""
Assert that the given value is a redirection status (between 300 and 399)
"""
msg = self._formatMessage(msg, "%s is not a redirection status" % safe_repr(status_code))
self.assertTrue(status_code >= 300 and status_code < 400, msg=msg)
def assert4XX(self, status_code, msg=None):
"""
Assert that the given value is a client error status (between 400 and 499)
"""
msg = self._formatMessage(msg, "%s is not a client error status" % safe_repr(status_code))
self.assertTrue(status_code >= 400 and status_code < 500, msg=msg)
def assert5XX(self, status_code, msg=None):
"""
Assert that the given value is a server error status (between 500 and 599)
"""
msg = self._formatMessage(msg, "%s is not a server error status" % safe_repr(status_code))
self.assertTrue(status_code >= 500 and status_code < 600, msg=msg)
......@@ -5,11 +5,12 @@ from uuid import uuid4
from pytz import UTC
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.django import editable_modulestore
from xmodule.course_module import CourseDescriptor
from xblock.core import Scope
from xmodule.x_module import XModuleDescriptor
class XModuleCourseFactory(Factory):
"""
Factory for XModule courses.
......@@ -25,10 +26,7 @@ class XModuleCourseFactory(Factory):
display_name = kwargs.pop('display_name', None)
location = Location('i4x', org, number, 'course', Location.clean(display_name))
try:
store = modulestore('direct')
except KeyError:
store = modulestore()
store = editable_modulestore('direct')
# Write the data to the mongo datastore
new_course = store.create_xmodule(location)
......@@ -117,7 +115,7 @@ class XModuleItemFactory(Factory):
if not isinstance(data, basestring):
data.update(template.get('data'))
store = modulestore('direct')
store = editable_modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location)
......
......@@ -10,7 +10,9 @@ from xblock.runtime import KeyValueStore, InvalidScopeError
from xmodule.tests import DATA_DIR
from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.draft import DraftModuleStore
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
from xmodule.contentstore.mongo import MongoContentStore
from xmodule.modulestore.tests.test_modulestore import check_path_to_location
......@@ -35,7 +37,7 @@ class TestMongoModuleStore(object):
# is ok only as long as none of the tests modify the db.
# If (when!) that changes, need to either reload the db, or load
# once and copy over to a tmp db for each test.
cls.store = cls.initdb()
cls.store, cls.content_store, cls.draft_store = cls.initdb()
@classmethod
def teardownClass(cls):
......@@ -46,10 +48,28 @@ class TestMongoModuleStore(object):
def initdb():
# connect to the db
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS)
# since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class
# as well
content_store = MongoContentStore(HOST, DB)
#
# Also test draft store imports
#
draft_store = DraftModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS)
# Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple']
import_from_xml(store, DATA_DIR, courses)
return store
courses = ['toy', 'simple', 'simple_with_draft']
import_from_xml(store, DATA_DIR, courses, draft_store=draft_store, static_content_store=content_store)
# also test a course with no importing of static content
import_from_xml(
store,
DATA_DIR,
['test_import_course'],
static_content_store=content_store,
do_import_static=False,
verbose=True
)
return store, content_store, draft_store
@staticmethod
def destroy_db(connection):
......@@ -77,10 +97,12 @@ class TestMongoModuleStore(object):
def test_get_courses(self):
'''Make sure the course objects loaded properly'''
courses = self.store.get_courses()
assert_equals(len(courses), 2)
assert_equals(len(courses), 4)
courses.sort(key=lambda c: c.id)
assert_equals(courses[0].id, 'edX/simple/2012_Fall')
assert_equals(courses[1].id, 'edX/toy/2012_Fall')
assert_equals(courses[1].id, 'edX/simple_with_draft/2012_Fall')
assert_equals(courses[2].id, 'edX/test_import_course/2012_Fall')
assert_equals(courses[3].id, 'edX/toy/2012_Fall')
def test_loads(self):
assert_not_equals(
......@@ -112,6 +134,13 @@ class TestMongoModuleStore(object):
'''Make sure that path_to_location works'''
check_path_to_location(self.store)
def test_xlinter(self):
'''
Run through the xlinter, we know the 'toy' course has violations, but the
number will continue to grow over time, so just check > 0
'''
assert_not_equals(perform_xlint(DATA_DIR, ['toy']), 0)
def test_get_courses_has_no_templates(self):
courses = self.store.get_courses()
for course in courses:
......@@ -129,7 +158,7 @@ class TestMongoModuleStore(object):
Assumes the information is desired for courses[1] ('toy' course).
"""
return courses[1].tabs[index]['name']
return courses[2].tabs[index]['name']
# There was a bug where model.save was not getting called after the static tab name
# was set set for tabs that have a URL slug. 'Syllabus' and 'Resources' fall into that
......
......@@ -206,20 +206,49 @@ class CombinedOpenEndedRubric(object):
def render_combined_rubric(self, rubric_xml, scores, score_types, feedback_types):
success, score_tuples = CombinedOpenEndedRubric.reformat_scores_for_rendering(scores, score_types,
feedback_types)
#Get all the categories in the rubric
rubric_categories = self.extract_categories(rubric_xml)
#Get a list of max scores, each entry belonging to a rubric category
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
actual_scores = []
#Get the highest possible score across all categories
max_score = max(max_scores)
for i in xrange(0, len(rubric_categories)):
category = rubric_categories[i]
for j in xrange(0, len(category['options'])):
#Loop through each category
for i, category in enumerate(rubric_categories):
#Loop through each option in the category
for j in xrange(len(category['options'])):
#Intialize empty grader types list
rubric_categories[i]['options'][j]['grader_types'] = []
for tuple in score_tuples:
if tuple[1] == i and tuple[2] == j:
for grader_type in tuple[3]:
#Score tuples are a flat data structure with (category, option, grader_type_list) for selected graders
for tup in score_tuples:
if tup[1] == i and tup[2] == j:
for grader_type in tup[3]:
#Set the rubric grader type to the tuple grader types
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
#Grab the score and add it to the actual scores. J will be the score for the selected
#grader type
if len(actual_scores)<=i:
#Initialize a new list in the list of lists
actual_scores.append([j])
else:
#If a list in the list of lists for this position exists, append to it
actual_scores[i] += [j]
actual_scores = [sum(i) / len(i) for i in actual_scores]
correct = []
#Define if the student is "correct" (1) "incorrect" (0) or "partially correct" (.5)
for (i, a) in enumerate(actual_scores):
if int(a) == max_scores[i]:
correct.append(1)
elif int(a)==0:
correct.append(0)
else:
correct.append(.5)
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{'categories': rubric_categories,
'max_scores': max_scores,
'correct' : correct,
'has_score': True,
'view_only': True,
'max_score': max_score,
......
......@@ -11,6 +11,9 @@ log = logging.getLogger(__name__)
class GradingServiceError(Exception):
"""
Exception for grading service. Shown when Open Response Assessment servers cannot be reached.
"""
pass
......@@ -62,7 +65,6 @@ class GradingService(object):
"""
Make a get request to the grading controller
"""
log.debug(params)
op = lambda: self.session.get(url,
allow_redirects=allow_redirects,
params=params)
......
......@@ -641,6 +641,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
"""
# Once we close the problem, we should not allow students
# to save answers
error_message = ""
closed, msg = self.check_if_closed()
if closed:
return msg
......@@ -650,17 +651,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# add new history element with answer and empty score and hint.
success, data = self.append_image_to_student_answer(data)
error_message = ""
if success:
success, allowed_to_submit, error_message = self.check_if_student_can_submit()
if allowed_to_submit:
data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer'])
self.new_history_entry(data['student_answer'])
self.send_to_grader(data['student_answer'], system)
self.change_state(self.ASSESSING)
else:
# Error message already defined
success = False
data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer'])
self.new_history_entry(data['student_answer'])
self.send_to_grader(data['student_answer'], system)
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."
......@@ -668,7 +663,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
return {
'success': success,
'error': error_message,
'student_response': data['student_answer']
'student_response': data['student_answer'].replace("\n","<br/>")
}
def update_score(self, data, system):
......@@ -699,12 +694,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
if self.child_state == self.ASSESSING:
eta_string = self.get_eta()
eta_string = "Your response has been submitted. Please check back later for your grade."
else:
post_assessment = ""
correct = ""
previous_answer = self.initial_display
previous_answer = ""
previous_answer = previous_answer.replace("\n","<br/>")
context = {
'prompt': self.child_prompt,
'previous_answer': previous_answer,
......
......@@ -58,7 +58,7 @@ class OpenEndedChild(object):
'assessing': 'In progress',
'post_assessment': 'Done',
'done': 'Done',
}
}
def __init__(self, system, location, definition, descriptor, static_data,
instance_state=None, shared_state=None, **kwargs):
......@@ -179,10 +179,11 @@ class OpenEndedChild(object):
answer = autolink_html(answer)
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
whitelist_tags=set(['embed', 'iframe', 'a', 'img', 'br']))
clean_html = cleaner.clean_html(answer)
clean_html = re.sub(r'</p>$', '', re.sub(r'^<p>', '', clean_html))
except:
clean_html = re.sub("\n","<br/>", 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)
......
......@@ -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}
......@@ -61,6 +61,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
else:
previous_answer = ''
previous_answer = previous_answer.replace("\n","<br/>")
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","<br/>")
}
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]
......
......@@ -46,7 +46,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.",
......@@ -189,9 +188,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 +227,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 +310,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 +509,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 +520,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 +548,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,
......
......@@ -73,6 +73,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 +204,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 +411,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,
......@@ -536,6 +538,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 +572,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 +582,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", {})
......@@ -617,7 +619,6 @@ 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)
module.handle_ajax("get_status", {})
#Move to the next step in the problem
try:
......@@ -660,15 +661,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 +683,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 +700,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 +707,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", {})
......
......@@ -89,12 +89,6 @@ class RoundTripTestCase(unittest.TestCase):
print("Checking module equality")
for location in initial_import.modules[course_id].keys():
print("Checking", location)
if location.category == 'html':
print(
"Skipping html modules--they can't import in"
" final form without writing files..."
)
continue
self.assertEquals(initial_import.modules[course_id][location],
second_import.modules[course_id][location])
......
......@@ -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):
......
This is a simple, but non-trivial, course using multiple module types and some nested structure.
<course name="A Simple Course" org="edX" course="simple_with_draft" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall">
<chapter name="Overview">
<video name="Welcome" youtube_id_0_75="izygArpw-Qo" youtube_id_1_0="p2Q6BrNhdh8" youtube_id_1_25="1EeWXzPdhSA" youtube_id_1_5="rABDYkeK0x8"/>
<videosequence format="Lecture Sequence" name="A simple sequence">
<html name="toylab" filename="toylab"/>
<video name="S0V1: Video Resources" youtube_id_0_75="EuzkdzfR0i8" youtube_id_1_0="1bK-WdDi6Qw" youtube_id_1_25="0v1VzoDVUTM" youtube_id_1_5="Bxk_-ZJb240"/>
</videosequence>
<section name="Lecture 2">
<sequential>
<video youtube_id_1_0="TBvX7HzxexQ"/>
<problem name="L1 Problem 1" points="1" type="lecture" showanswer="attempted" filename="L1_Problem_1" rerandomize="never"/>
</sequential>
</section>
</chapter>
<chapter name="Chapter 2" url_name='chapter_2'>
<section name="Problem Set 1">
<sequential>
<problem type="lecture" showanswer="attempted" rerandomize="true" display_name="A simple coding problem" name="Simple coding problem" filename="ps01-simple" url_name="ps01-simple"/>
</sequential>
</section>
<video name="Lost Video" youtube_id_1_0="TBvX7HzxexQ"/>
<sequential format="Lecture Sequence" url_name='test_sequence'>
<vertical url_name='test_vertical'>
<html url_name='test_html'>
Foobar
</html>
</vertical>
</sequential>
</chapter>
</course>
<vertical url_name='test_vertical' parent_sequential_url='i4x://edX/simple_with_draft/sequential/test_sequence' index_in_children_list="0">
<html url_name='test_html'>
Foobar - edit in draft
</html>
</vertical>
\ No newline at end of file
<b>Lab 2A: Superposition Experiment</b>
<p>Isn't the toy course great?</p>
<?xml version="1.0"?>
<problem>
<p>
<h1>Finger Exercise 1</h1>
</p>
<p>
Here are two definitions: </p>
<ol class="enumerate">
<li>
<p>
Declarative knowledge refers to statements of fact. </p>
</li>
<li>
<p>
Imperative knowledge refers to 'how to' methods. </p>
</li>
</ol>
<p>
Which of the following choices is correct? </p>
<ol class="enumerate">
<li>
<p>
Statement 1 is true, Statement 2 is false </p>
</li>
<li>
<p>
Statement 1 is false, Statement 2 is true </p>
</li>
<li>
<p>
Statement 1 and Statement 2 are both false </p>
</li>
<li>
<p>
Statement 1 and Statement 2 are both true </p>
</li>
</ol>
<p>
<symbolicresponse answer="4">
<textline size="90" math="1"/>
</symbolicresponse>
</p>
</problem>
<problem><style media="all" type="text/css"/>
<text><h2>Paying Off Credit Card Debt</h2>
<p> Each month, a credit
card statement will come with the option for you to pay a
minimum amount of your charge, usually 2% of the balance due.
However, the credit card company earns money by charging
interest on the balance that you don't pay. So even if you
pay credit card payments on time, interest is still accruing
on the outstanding balance.</p>
<p >Say you've made a
$5,000 purchase on a credit card with 18% annual interest
rate and 2% minimum monthly payment rate. After a year, how
much is the remaining balance? Use the following
equations.</p>
<blockquote>
<p><strong>Minimum monthly payment</strong>
= (Minimum monthly payment rate) x (Balance)<br/>
(Minimum monthly payment gets split into interest paid and
principal paid)<br/>
<strong>Interest Paid</strong> = (Annual interest rate) / (12
months) x (Balance)<br/>
<strong>Principal paid</strong> = (Minimum monthly payment) -
(Interest paid)<br/>
<strong>Remaining balance</strong> = Balance - (Principal
paid)</p>
</blockquote>
<p >For month 1, compute the minimum monthly payment by taking 2% of the balance.</p>
<blockquote xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">
<p><strong>Minimum monthly payment</strong>
= .02 x $5000 = $100</p>
<p>We can't simply deduct this from the balance because
there is compounding interest. Of this $100 monthly
payment, compute how much will go to paying off interest
and how much will go to paying off the principal. Remember
that it's the annual interest rate that is given, so we
need to divide it by 12 to get the monthly interest
rate.</p>
<p><strong>Interest paid</strong> = .18/12 x $5000 =
$75<br/>
<strong>Principal paid</strong> = $100 - $75 = $25</p>
<p>The remaining balance at the end of the first month will
be the principal paid this month subtracted from the
balance at the start of the month.</p>
<p><strong>Remaining balance</strong> = $5000 - $25 =
$4975</p>
</blockquote>
<p xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">For month 2, we
repeat the same steps.</p>
<blockquote xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">
<p><strong>Minimum monthly payment</strong>
= .02 x $4975 = $99.50<br/>
<strong>Interest Paid</strong> = .18/12 x $4975 =
$74.63<br/>
<strong>Principal Paid</strong> = $99.50 - $74.63 =
$24.87<br/>
<strong>Remaining Balance</strong> = $4975 - $24.87 =
$4950.13</p>
</blockquote>
<p xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">After 12 months, the
total amount paid is $1167.55, leaving an outstanding balance
of $4708.10. Pretty depressing!</p>
</text></problem>
<sequential>
<sequential filename='vertical_sequential' slug='vertical_sequential' />
</sequential>
\ No newline at end of file
<course org="edX" course="test_import_course" url_name="2012_Fall"/>
\ No newline at end of file
<course>
<textbook title="Textbook" book_url="https://s3.amazonaws.com/edx-textbooks/guttag_computation_v3/"/>
<chapter url_name="Overview">
<videosequence url_name="Toy_Videos">
<html url_name="secret:toylab"/>
<html url_name="toyjumpto"/>
<html url_name="toyhtml"/>
<html url_name="nonportable"/>
<html url_name="nonportable_link"/>
<video url_name="Video_Resources" youtube_id_1_0="1bK-WdDi6Qw" display_name="Video Resources"/>
</videosequence>
<video url_name="Welcome" youtube_id_1_0="p2Q6BrNhdh8" display_name="Welcome"/>
<video url_name="video_123456789012" youtube_id_1_0="p2Q6BrNhdh8" display_name='Test Video'/>
<video url_name="video_4f66f493ac8f" youtube_id_1_0="p2Q6BrNhdh8"/>
</chapter>
<chapter url_name="secret:magic"/>
<chapter url_name="poll_test"/>
<chapter url_name="vertical_container"/>
<chapter url_name="handout_container"/>
</course>
<a href='/static/handouts/sample_handout.txt'>Sample</a>
\ No newline at end of file
{
"course/2012_Fall": {
"graceperiod": "2 days 5 hours 59 minutes 59 seconds",
"start": "2015-07-17T12:00",
"display_name": "Toy Course",
"graded": "true",
"tabs": [
{"type": "courseware"},
{"type": "course_info", "name": "Course Info"},
{"type": "static_tab", "url_slug": "syllabus", "name": "Syllabus"},
{"type": "static_tab", "url_slug": "resources", "name": "Resources"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}
]
},
"chapter/Overview": {
"display_name": "Overview"
},
"videosequence/Toy_Videos": {
"display_name": "Toy Videos",
"format": "Lecture Sequence"
},
"html/secret:toylab": {
"display_name": "Toy lab"
},
"video/Video_Resources": {
"display_name": "Video Resources"
},
"video/Welcome": {
"display_name": "Welcome"
}
}
<sequential>
<vertical filename="vertical_test" slug="vertical_test" />
<html slug="unicode"></html>
</sequential>
\ No newline at end of file
<sequential>
<video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/>
<video url_name="separate_file_video"/>
<poll_question name="T1_changemind_poll_foo_2" display_name="Change your answer" reset="false">
<p>Have you changed your mind?</p>
<answer id="yes">Yes</answer>
<answer id="no">No</answer>
</poll_question>
</sequential>
<video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/>
......@@ -8,6 +8,8 @@
<html url_name="nonportable"/>
<html url_name="nonportable_link"/>
<html url_name="badlink"/>
<html url_name="with_styling"/>
<html url_name="just_img"/>
<video url_name="Video_Resources" youtube_id_1_0="1bK-WdDi6Qw" display_name="Video Resources"/>
</videosequence>
<video url_name="Welcome" youtube_id_1_0="p2Q6BrNhdh8" display_name="Welcome"/>
......
<img src="/static/foo_bar.jpg" />
\ No newline at end of file
<html filename="just_img.html"/>
\ No newline at end of file
<p style="font:italic bold 72px/30px Georgia, serif; color: red; ">Red text here</p>
\ No newline at end of file
<html filename="with_styling.html"/>
\ No newline at end of file
......@@ -136,6 +136,25 @@ To run a single nose test:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
To run a single test and get stdout, with proper env config:
python manage.py cms --settings test test contentstore.tests.test_import_nostatic -s
To run a single test and get stdout and get coverage:
python -m coverage run --rcfile=./common/lib/xmodule/.coveragerc which ./manage.py cms --settings test test --traceback --logging-clear-handlers --liveserver=localhost:8000-9000 contentstore.tests.test_import_nostatic -s # cms example
python -m coverage run --rcfile=./lms/.coveragerc which ./manage.py lms --settings test test --traceback --logging-clear-handlers --liveserver=localhost:8000-9000 courseware.tests.test_module_render -s # lms example
generate coverage report:
coverage report --rcfile=./common/lib/xmodule/.coveragerc
or to get html report:
coverage html --rcfile=./common/lib/xmodule/.coveragerc
then browse reports/common/lib/xmodule/cover/index.html
Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out [the pdb documentation](http://docs.python.org/library/pdb.html)
......
......@@ -30,10 +30,22 @@ TESTS_FAILED=0
# /usr/bin/Xvfb :1 -screen 0 1024x268x24
# This allows us to run Chrome without a display
export DISPLAY=:1
SKIP_TESTS=""
# Testing for the existance of these environment variables
if [ ! -z ${LETTUCE_BROWSER+x} ]; then
SKIP_TESTS="--tag -skip_$LETTUCE_BROWSER"
fi
if [ ! -z ${SAUCE_ENABLED+x} ]; then
# SAUCE_INFO is a - seperated string PLATFORM-BROWSER-VERSION-DEVICE
# Error checking is done in the setting up of the browser
IFS='-' read -a SAUCE <<< "${SAUCE_INFO}"
SKIP_TESTS="--tag -skip_sauce --tag -skip_${SAUCE[1]}"
fi
# Run the lms and cms acceptance tests
# (the -v flag turns off color in the output)
rake test_acceptance_lms["-v 3"] || TESTS_FAILED=1
rake test_acceptance_cms["-v 3"] || TESTS_FAILED=1
rake test_acceptance_lms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
rake test_acceptance_cms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
[ $TESTS_FAILED == '0' ]
......@@ -3,21 +3,18 @@ from django.test.utils import override_settings
import xmodule.modulestore.django
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE
from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.modulestore.django import modulestore
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class WikiRedirectTestCase(LoginEnrollmentTestCase):
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
def find_course(name):
"""Assumes the course is present"""
return [c for c in courses if c.location.course == name][0]
def setUp(self):
self.toy = find_course("toy")
# Load the toy course
self.toy = modulestore().get_course('edX/toy/2012_Fall')
# Create two accounts
self.student = 'view@test.com'
......
......@@ -81,8 +81,8 @@ def get_opt_course_with_access(user, course_id, action):
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
return '/static/' + course.data_dir + "/images/course_image.jpg"
if course.lms.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
return '/static/' + (course.lms.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg"
else:
loc = course.location._replace(tag='c4x', category='asset', name=course.course_image)
_path = StaticContent.get_url_path_from_location(loc)
......@@ -156,7 +156,8 @@ def get_course_about_section(course, section_key):
model_data_cache,
course.id,
not_found_ok=True,
wrap_xmodule_display=False
wrap_xmodule_display=False,
static_asset_path=course.lms.static_asset_path
)
html = ''
......@@ -204,7 +205,8 @@ def get_course_info_section(request, course, section_key):
loc,
model_data_cache,
course.id,
wrap_xmodule_display=False
wrap_xmodule_display=False,
static_asset_path=course.lms.static_asset_path
)
html = ''
......@@ -242,7 +244,8 @@ def get_course_syllabus_section(course, section_key):
return replace_static_urls(
htmlFile.read().decode('utf-8'),
getattr(course, 'data_dir', None),
course_id=course.location.course_id
course_id=course.location.course_id,
static_asset_path=course.lms.static_asset_path,
)
except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format(
......
......@@ -11,7 +11,8 @@ Feature: Login in as a registered user
And I submit my credentials on the login form
Then I should see the login error message "This account has not been activated"
# CHROME ONLY, firefox will not redirect properly
# firefox will not redirect properly when the whole suite is run
@skip_firefox
Scenario: Login to an activated account
Given I am an edX user
And I am an activated user
......
......@@ -3,7 +3,8 @@ Feature: Sign in
As a new user
I want to signup for a student account
# CHROME ONLY, firefox will not redirect properly
# firefox will not redirect properly
@skip_firefox
Scenario: Sign up from the homepage
Given I visit the homepage
When I click the link with the text "Register Now"
......
......@@ -11,6 +11,8 @@ Feature: Video component
Given the course has a Video component in Youtube mode
Then when I view the video it has rendered in Youtube mode
# Firefox doesn't have HTML5
@skip_firefox
Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component in HTML5 mode
Then when I view the video it has autoplay enabled
\ No newline at end of file
Then when I view the video it has autoplay enabled
......@@ -124,7 +124,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_
def get_module(user, request, location, model_data_cache, course_id,
position=None, not_found_ok=False, wrap_xmodule_display=True,
grade_bucket_type=None, depth=0):
grade_bucket_type=None, depth=0,
static_asset_path=''):
"""
Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none
......@@ -141,6 +142,10 @@ def get_module(user, request, location, model_data_cache, course_id,
position within module
- depth : number of levels of descendents to cache when loading this module.
None means cache all descendents
- static_asset_path : static asset path to use (overrides descriptor's value); needed
by get_course_info_section, because info section modules
do not have a course as the parent module, and thus do not
inherit this lms key value.
Returns: xmodule instance, or None if the user does not have access to the
module. If there's an error, will try to return an instance of ErrorModule
......@@ -152,7 +157,8 @@ def get_module(user, request, location, model_data_cache, course_id,
return get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
position=position,
wrap_xmodule_display=wrap_xmodule_display,
grade_bucket_type=grade_bucket_type)
grade_bucket_type=grade_bucket_type,
static_asset_path=static_asset_path)
except ItemNotFoundError:
if not not_found_ok:
log.exception("Error in get_module")
......@@ -179,7 +185,8 @@ def get_xqueue_callback_url_prefix(request):
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
static_asset_path=''):
"""
Implements get_module, extracting out the request-specific functionality.
......@@ -194,12 +201,14 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, xqueue_callback_url_prefix,
position, wrap_xmodule_display, grade_bucket_type)
position, wrap_xmodule_display, grade_bucket_type,
static_asset_path)
def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, xqueue_callback_url_prefix,
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
static_asset_path=''):
"""
Actually implement get_module, without requiring a request.
......@@ -282,7 +291,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
# inner_get_module, not the parent's callback. Add it as an argument....
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, make_xqueue_callback,
position, wrap_xmodule_display, grade_bucket_type)
position, wrap_xmodule_display, grade_bucket_type,
static_asset_path)
def xblock_model_data(descriptor):
return DbModel(
......@@ -349,6 +359,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
static_replace.replace_static_urls,
data_directory=getattr(descriptor, 'data_dir', None),
course_id=course_id,
static_asset_path=static_asset_path or descriptor.lms.static_asset_path,
),
replace_course_urls=partial(
static_replace.replace_course_urls,
......@@ -407,7 +418,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
module.get_html = replace_static_urls(
_get_html,
getattr(descriptor, 'data_dir', None),
course_id=course_id
course_id=course_id,
static_asset_path=static_asset_path or descriptor.lms.static_asset_path
)
# Allow URLs of the form '/course/' refer to the root of multicourse directory
......
......@@ -380,7 +380,8 @@ def get_static_tab_contents(request, course, tab):
loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug'])
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course.id,
request.user, modulestore().get_instance(course.id, loc), depth=0)
tab_module = get_module(request.user, request, loc, model_data_cache, course.id)
tab_module = get_module(request.user, request, loc, model_data_cache, course.id,
static_asset_path=course.lms.static_asset_path)
logging.debug('course_module = {0}'.format(tab_module))
......
......@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse
from django.test.client import Client
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
from xmodule.tests import get_test_system
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
......@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class BaseTestXmodule(ModuleStoreTestCase):
"""Base class for testing Xmodules with mongo store.
......
from xmodule.modulestore.tests.django_utils import xml_store_config, mongo_store_config, draft_mongo_store_config
"""
Define test configuration for modulestores.
"""
from xmodule.modulestore.tests.django_utils import xml_store_config, \
mongo_store_config, draft_mongo_store_config,\
mixed_store_config
from django.conf import settings
......@@ -6,3 +12,15 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
# Map all XML course fixtures so they are accessible through
# the MixedModuleStore
MAPPINGS = {
'edX/toy/2012_Fall': 'xml',
'edX/toy/TT_2012_Fall': 'xml',
'edX/test_end/2012_Fall': 'xml',
'edX/test_about_blob_end_date/2012_Fall': 'xml',
'edX/graded/2012_Fall': 'xml',
'edX/open_ended/2012_Fall': 'xml',
}
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, MAPPINGS)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment