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 ...@@ -2,6 +2,7 @@ Feature: Advanced (manual) course policy
In order to specify course policy settings for which no custom user interface exists 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 I want to be able to manually enter JSON key /value pairs
Scenario: A course author sees default advanced settings Scenario: A course author sees default advanced settings
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I select the Advanced Settings When I select the Advanced Settings
...@@ -11,6 +12,8 @@ Feature: Advanced (manual) course policy ...@@ -11,6 +12,8 @@ Feature: Advanced (manual) course policy
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
Then the settings are alphabetized Then the settings are alphabetized
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test cancel editing key value Scenario: Test cancel editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
...@@ -19,6 +22,8 @@ Feature: Advanced (manual) course policy ...@@ -19,6 +22,8 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is unchanged Then the policy key value is unchanged
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test editing key value Scenario: Test editing key value
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key and save When I edit the value of a policy key and save
...@@ -26,6 +31,8 @@ Feature: Advanced (manual) course policy ...@@ -26,6 +31,8 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then the policy key value is changed Then the policy key value is changed
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value for "discussion_topics" When I create a JSON object as a value for "discussion_topics"
...@@ -33,6 +40,8 @@ Feature: Advanced (manual) course policy ...@@ -33,6 +40,8 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then it is displayed as formatted 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 Scenario: Test error if value supplied is of the wrong type
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value for "display_name" When I create a JSON object as a value for "display_name"
...@@ -41,6 +50,8 @@ Feature: Advanced (manual) course policy ...@@ -41,6 +50,8 @@ Feature: Advanced (manual) course policy
Then the policy key value is unchanged Then the policy key value is unchanged
# This feature will work in Firefox only when Firefox is the active window # 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 Scenario: Test automatic quoting of non-JSON values
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a non-JSON value not in quotes When I create a non-JSON value not in quotes
...@@ -48,6 +59,8 @@ Feature: Advanced (manual) course policy ...@@ -48,6 +59,8 @@ Feature: Advanced (manual) course policy
And I reload the page And I reload the page
Then it is displayed as a string Then it is displayed as a string
# Sauce labs does not play nicely with CodeMirror
@skip_sauce
Scenario: Confirmation is shown on save Scenario: Confirmation is shown on save
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I edit the value of a policy key When I edit the value of a policy key
......
...@@ -10,7 +10,10 @@ Feature: Course checklists ...@@ -10,7 +10,10 @@ Feature: Course checklists
Then I can check and uncheck tasks in a checklist Then I can check and uncheck tasks in a checklist
And They are correctly selected after reloading the page 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 Scenario: A task can link to a location within Studio
Given I have opened Checklists Given I have opened Checklists
When I select a link to the course outline When I select a link to the course outline
...@@ -18,7 +21,10 @@ Feature: Course checklists ...@@ -18,7 +21,10 @@ Feature: Course checklists
And I press the browser back button And I press the browser back button
Then I am brought back to the course outline in the correct state 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 Scenario: A task can link to a location outside Studio
Given I have opened Checklists Given I have opened Checklists
When I select a link to help page When I select a link to help page
......
...@@ -64,6 +64,10 @@ Feature: Course Overview ...@@ -64,6 +64,10 @@ Feature: Course Overview
And I change an assignment's grading status And I change an assignment's grading status
Then I am shown a notification 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 Scenario: Notification is shown on subsection reorder
Given I have opened a new course section in Studio Given I have opened a new course section in Studio
And I have added a new subsection And I have added a new subsection
......
Feature: Course Settings Feature: Course Settings
As a course author, I want to be able to configure my 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 Scenario: User can set course dates
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I select Schedule and Details When I select Schedule and Details
...@@ -8,12 +10,16 @@ Feature: Course Settings ...@@ -8,12 +10,16 @@ Feature: Course Settings
And I press the "Save" notification button And I press the "Save" notification button
Then I see the set dates on refresh 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) Scenario: User can clear previously set course dates (except start date)
Given I have set course dates Given I have set course dates
And I clear all the dates except start And I clear all the dates except start
And I press the "Save" notification button And I press the "Save" notification button
Then I see cleared dates on refresh Then I see cleared dates on refresh
# IE has trouble with saving information
@skip_internetexplorer
Scenario: User cannot clear the course start date Scenario: User cannot clear the course start date
Given I have set course dates Given I have set course dates
And I press the "Save" notification button And I press the "Save" notification button
...@@ -21,6 +27,10 @@ Feature: Course Settings ...@@ -21,6 +27,10 @@ Feature: Course Settings
Then I receive a warning about course start date Then I receive a warning about course start date
And The previously set start date is shown on refresh 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 Scenario: User can correct the course start date warning
Given I have tried to clear the course start Given I have tried to clear the course start
And I have entered a new course start date And I have entered a new course start date
...@@ -28,12 +38,16 @@ Feature: Course Settings ...@@ -28,12 +38,16 @@ Feature: Course Settings
Then The warning about course start date goes away Then The warning about course start date goes away
And My new course start date is shown on refresh 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 Scenario: Settings are only persisted when saved
Given I have set course dates Given I have set course dates
And I press the "Save" notification button And I press the "Save" notification button
When I change fields When I change fields
Then I do not see the new changes persisted on refresh 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 Scenario: Settings are reset on cancel
Given I have set course dates Given I have set course dates
And I press the "Save" notification button And I press the "Save" notification button
...@@ -41,6 +55,8 @@ Feature: Course Settings ...@@ -41,6 +55,8 @@ Feature: Course Settings
And I press the "Cancel" notification button And I press the "Cancel" notification button
Then I do not see the changes Then I do not see the changes
# Safari gets CSRF token errors
@skip_safari
Scenario: Confirmation is shown on save Scenario: Confirmation is shown on save
Given I have opened a new course in Studio Given I have opened a new course in Studio
When I select Schedule and Details When I select Schedule and Details
......
...@@ -91,7 +91,7 @@ def remove_course_team_admin(_step, outer_capture, name): ...@@ -91,7 +91,7 @@ def remove_course_team_admin(_step, outer_capture, name):
@step(u'"([^"]*)" logs in$') @step(u'"([^"]*)" logs in$')
def other_user_login(_step, name): def other_user_login(_step, name):
world.browser.cookies.delete() world.visit('logout')
world.visit('/') world.visit('/')
signin_css = 'a.action-signin' signin_css = 'a.action-signin'
......
Feature: Course updates Feature: Course updates
As a course author, I want to be able to provide updates to my students 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 Scenario: Users can add updates
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the course updates page And I go to the course updates page
...@@ -8,6 +10,8 @@ Feature: Course updates ...@@ -8,6 +10,8 @@ Feature: Course updates
Then I should see the update "Hello" Then I should see the update "Hello"
And I see a "saving" notification And I see a "saving" notification
# Internet explorer can't select all so the update appears weirdly
@skip_internetexplorer
Scenario: Users can edit updates Scenario: Users can edit updates
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the course updates page And I go to the course updates page
...@@ -33,6 +37,8 @@ Feature: Course updates ...@@ -33,6 +37,8 @@ Feature: Course updates
Then I should see the date "June 1, 2013" Then I should see the date "June 1, 2013"
And I see a "saving" notification And I see a "saving" notification
# Internet explorer can't select all so the update appears weirdly
@skip_internetexplorer
Scenario: Users can change handouts Scenario: Users can change handouts
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the course updates page And I go to the course updates page
......
...@@ -6,6 +6,8 @@ Feature: Discussion Component Editor ...@@ -6,6 +6,8 @@ Feature: Discussion Component Editor
And I edit and select Settings And I edit and select Settings
Then I see three alphabetized settings and their expected values 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 Scenario: User can modify display name
Given I have created a Discussion Tag Given I have created a Discussion Tag
And I edit and select Settings And I edit and select Settings
......
...@@ -13,7 +13,7 @@ Feature: Course Grading ...@@ -13,7 +13,7 @@ Feature: Course Grading
When I add "6" new grades When I add "6" new grades
Then I see I now have "5" 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 Scenario: Users can delete grading ranges
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I am viewing the grading settings And I am viewing the grading settings
...@@ -21,6 +21,9 @@ Feature: Course Grading ...@@ -21,6 +21,9 @@ Feature: Course Grading
And I delete a grade And I delete a grade
Then I see I now have "2" grades 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 Scenario: Users can move grading ranges
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I am viewing the grading settings And I am viewing the grading settings
...@@ -85,6 +88,9 @@ Feature: Course Grading ...@@ -85,6 +88,9 @@ Feature: Course Grading
When I change assignment type "Homework" to "" When I change assignment type "Homework" to ""
Then the save button is disabled 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 Scenario: User can edit grading range names
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I have populated the course And I have populated the course
......
...@@ -144,6 +144,7 @@ def cannot_edit_fail(_step): ...@@ -144,6 +144,7 @@ def cannot_edit_fail(_step):
pass # We should get this exception on failing to edit the element pass # We should get this exception on failing to edit the element
@step(u'I change the grace period to "(.*)"$') @step(u'I change the grace period to "(.*)"$')
def i_change_grace_period(_step, grace_period): def i_change_grace_period(_step, grace_period):
grace_period_css = '#course-grading-graceperiod' grace_period_css = '#course-grading-graceperiod'
......
...@@ -6,6 +6,8 @@ Feature: HTML Editor ...@@ -6,6 +6,8 @@ Feature: HTML Editor
And I edit and select Settings And I edit and select Settings
Then I see only the HTML display name setting Then I see only the HTML display name setting
# Safari doesn't save the name properly
@skip_safari
Scenario: User can modify display name Scenario: User can modify display name
Given I have created a Blank HTML Page Given I have created a Blank HTML Page
And I edit and select Settings And I edit and select Settings
......
...@@ -7,12 +7,16 @@ Feature: Problem Editor ...@@ -7,12 +7,16 @@ Feature: Problem Editor
Then I see five alphabetized settings and their expected values Then I see five alphabetized settings and their expected values
And Edit High Level Source is not visible 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 Scenario: User can modify String values
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
When I edit and select Settings When I edit and select Settings
Then I can modify the display name Then I can modify the display name
And my display name change is persisted on save 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 Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
When I edit and select Settings When I edit and select Settings
...@@ -25,6 +29,8 @@ Feature: Problem Editor ...@@ -25,6 +29,8 @@ Feature: Problem Editor
Then I can revert the display name to unset Then I can revert the display name to unset
And my display name is unset on save 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 Scenario: User can select values in a Select
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
When I edit and select Settings When I edit and select Settings
...@@ -32,6 +38,8 @@ Feature: Problem Editor ...@@ -32,6 +38,8 @@ Feature: Problem Editor
And my change to randomization is persisted And my change to randomization is persisted
And I can revert to the default value for randomization 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 Scenario: User can modify float input values
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
When I edit and select Settings When I edit and select Settings
...@@ -44,16 +52,22 @@ Feature: Problem Editor ...@@ -44,16 +52,22 @@ Feature: Problem Editor
When I edit and select Settings When I edit and select Settings
Then if I set the weight to "abc", it remains unset 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 Scenario: User cannot type decimal values integer number field
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
When I edit and select Settings When I edit and select Settings
Then if I set the max attempts to "2.34", it will persist as a valid integer 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 Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
When I edit and select Settings When I edit and select Settings
Then if I set the max attempts to "-3", it will persist as a valid integer 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 Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem Given I have created a Blank Common Problem
When I edit and select Settings When I edit and select Settings
...@@ -67,6 +81,8 @@ Feature: Problem Editor ...@@ -67,6 +81,8 @@ Feature: Problem Editor
Then Edit High Level Source is visible Then Edit High Level Source is visible
# This feature will work in Firefox only when Firefox is the active window # 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) Scenario: High Level source is persisted for LaTeX problem (bug STUD-280)
Given I have created a LaTeX Problem Given I have created a LaTeX Problem
When I edit and compile the High Level Source When I edit and compile the High Level Source
......
...@@ -15,6 +15,8 @@ Feature: Static Pages ...@@ -15,6 +15,8 @@ Feature: Static Pages
And I "delete" the "Empty" page And I "delete" the "Empty" page
Then I should not see a "Empty" static page Then I should not see a "Empty" static page
# Safari won't update the name properly
@skip_safari
Scenario: Users can edit static pages Scenario: Users can edit static pages
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the static pages page And I go to the static pages page
......
...@@ -25,6 +25,8 @@ Feature: Create Subsection ...@@ -25,6 +25,8 @@ Feature: Create Subsection
And I reload the page And I reload the page
Then I see it marked as Homework 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) Scenario: Set a due date in a different year (bug #256)
Given I have opened a new subsection in Studio Given I have opened a new subsection in Studio
And I set the subsection release date to 12/25/2011 03:00 And I set the subsection release date to 12/25/2011 03:00
......
...@@ -5,6 +5,9 @@ Feature: Textbooks ...@@ -5,6 +5,9 @@ Feature: Textbooks
When I go to the textbooks page When I go to the textbooks page
Then I should see a message telling me to create a new textbook 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 Scenario: Create a textbook
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the textbooks page And I go to the textbooks page
......
Feature: Upload Files Feature: Upload Files
As a course author, I want to be able to upload files for my students 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 Scenario: Users can upload files
Given I have opened a new course in Studio Given I have opened a new course in Studio
And I go to the files and uploads page And I go to the files and uploads page
...@@ -8,6 +10,8 @@ Feature: Upload Files ...@@ -8,6 +10,8 @@ Feature: Upload Files
Then I should see the file "test" was uploaded Then I should see the file "test" was uploaded
And The url for the file "test" is valid 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 Scenario: Users can update files
Given I have opened a new course in studio Given I have opened a new course in studio
And I go to the files and uploads page And I go to the files and uploads page
...@@ -15,6 +19,8 @@ Feature: Upload Files ...@@ -15,6 +19,8 @@ Feature: Upload Files
And I upload the file "test" And I upload the file "test"
Then I should see only one "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 Scenario: Users can delete uploaded files
Given I have opened a new course in studio Given I have opened a new course in studio
And I go to the files and uploads page And I go to the files and uploads page
...@@ -23,12 +29,16 @@ Feature: Upload Files ...@@ -23,12 +29,16 @@ Feature: Upload Files
Then I should not see the file "test" was uploaded Then I should not see the file "test" was uploaded
And I see a confirmation that the file was deleted 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 Scenario: Users can download files
Given I have opened a new course in studio Given I have opened a new course in studio
And I go to the files and uploads page And I go to the files and uploads page
When I upload the file "test" When I upload the file "test"
Then I can download the correct "test" file 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 Scenario: Users can download updated files
Given I have opened a new course in studio Given I have opened a new course in studio
And I go to the files and uploads page And I go to the files and uploads page
......
...@@ -10,6 +10,7 @@ import os ...@@ -10,6 +10,7 @@ import os
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
@step(u'I go to the files and uploads page') @step(u'I go to the files and uploads page')
def go_to_uploads(_step): def go_to_uploads(_step):
menu_css = 'li.nav-course-courseware' menu_css = 'li.nav-course-courseware'
...@@ -106,8 +107,8 @@ def get_index(file_name): ...@@ -106,8 +107,8 @@ def get_index(file_name):
def get_file(file_name): def get_file(file_name):
index = get_index(file_name) index = get_index(file_name)
assert index != -1 assert index != -1
url_css = 'a.filename' url_css = 'a.filename'
def get_url(): def get_url():
return world.css_find(url_css)[index]._element.get_attribute('href') return world.css_find(url_css)[index]._element.get_attribute('href')
url = world.retry_on_exception(get_url) url = world.retry_on_exception(get_url)
......
...@@ -6,17 +6,23 @@ Feature: Video Component Editor ...@@ -6,17 +6,23 @@ Feature: Video Component Editor
And I edit the component And I edit the component
Then I see the correct video settings and default values 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 Scenario: User can modify Video display name
Given I have created a Video component Given I have created a Video component
And I edit the component And I edit the component
Then I can modify the display name Then I can modify the display name
And my video display name change is persisted on save 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 Scenario: Captions are hidden when "show captions" is false
Given I have created a Video component Given I have created a Video component
And I have set "show captions" to False And I have set "show captions" to False
Then when I view the video it does not show the captions 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 Scenario: Captions are shown when "show captions" is true
Given I have created a Video component Given I have created a Video component
And I have set "show captions" to True And I have set "show captions" to True
......
...@@ -10,15 +10,21 @@ Feature: Video Component ...@@ -10,15 +10,21 @@ Feature: Video Component
Given I have clicked the new unit button Given I have clicked the new unit button
Then creating a video takes a single click Then creating a video takes a single click
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden correctly Scenario: Captions are hidden correctly
Given I have created a Video component Given I have created a Video component
And I have hidden captions And I have hidden captions
Then when I view the video it does not show the 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 Scenario: Captions are shown correctly
Given I have created a Video component Given I have created a Video component
Then when I view the video it does show the captions Then when I view the video it does show the captions
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are toggled correctly Scenario: Captions are toggled correctly
Given I have created a Video component Given I have created a Video component
And I have toggled captions And I have toggled captions
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Script for importing courseware from XML format 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.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
...@@ -14,18 +14,26 @@ class Command(BaseCommand): ...@@ -14,18 +14,26 @@ class Command(BaseCommand):
""" """
help = 'Import the specified data directory into the default ModuleStore' 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): def handle(self, *args, **options):
"Execute the command" "Execute the command"
if len(args) == 0: 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] data_dir = args[0]
do_import_static = not (options.get('nostatic', False))
if len(args) > 1: if len(args) > 1:
course_dirs = args[1:] course_dirs = args[1:]
else: else:
course_dirs = None course_dirs = None
print("Importing. Data_dir={data}, course_dirs={courses}".format( print("Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir, 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, 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): ...@@ -60,11 +60,11 @@ class UploadTestCase(CourseTestCase):
f = BytesIO("sample content") f = BytesIO("sample content")
f.name = "sample.txt" f.name = "sample.txt"
resp = self.client.post(self.url, {"name": "my-name", "file": f}) resp = self.client.post(self.url, {"name": "my-name", "file": f})
self.assert2XX(resp.status_code) self.assertEquals(resp.status_code, 200)
def test_no_file(self): def test_no_file(self):
resp = self.client.post(self.url, {"name": "file.txt"}) resp = self.client.post(self.url, {"name": "file.txt"})
self.assert4XX(resp.status_code) self.assertEquals(resp.status_code, 400)
def test_get(self): def test_get(self):
resp = self.client.get(self.url) resp = self.client.get(self.url)
......
...@@ -486,7 +486,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -486,7 +486,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
content_store = contentstore() content_store = contentstore()
module_store = modulestore('direct') 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_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
course = module_store.get_item(course_location) course = module_store.get_item(course_location)
...@@ -962,8 +962,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -962,8 +962,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'vertical', 'vertical_test', None]), depth=1) 'vertical', 'vertical_test', None]), depth=1)
self.assertTrue(getattr(vertical, 'is_draft', False)) 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(): for child in vertical.get_children():
self.assertTrue(getattr(child, 'is_draft', False)) 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 # make sure that we don't have a sequential that is in draft mode
sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy', sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy',
...@@ -1074,6 +1083,38 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1074,6 +1083,38 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# It should now contain empty data # It should now contain empty data
self.assertEquals(imported_word_cloud.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): def test_course_handouts_rewrites(self):
module_store = modulestore('direct') module_store = modulestore('direct')
...@@ -1384,7 +1425,7 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1384,7 +1425,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
self.assertContains(resp, 'Chapter 2') self.assertContains(resp, 'Chapter 2')
# go to various pages # go to various pages
...@@ -1394,92 +1435,92 @@ class ContentStoreTest(ModuleStoreTestCase): ...@@ -1394,92 +1435,92 @@ class ContentStoreTest(ModuleStoreTestCase):
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# export page # export page
resp = self.client.get(reverse('export_course', resp = self.client.get(reverse('export_course',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# manage users # manage users
resp = self.client.get(reverse('manage_users', resp = self.client.get(reverse('manage_users',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# course info # course info
resp = self.client.get(reverse('course_info', resp = self.client.get(reverse('course_info',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# settings_details # settings_details
resp = self.client.get(reverse('settings_details', resp = self.client.get(reverse('settings_details',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# settings_details # settings_details
resp = self.client.get(reverse('settings_grading', resp = self.client.get(reverse('settings_grading',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# static_pages # static_pages
resp = self.client.get(reverse('static_pages', resp = self.client.get(reverse('static_pages',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'coursename': loc.name})) 'coursename': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# static_pages # static_pages
resp = self.client.get(reverse('asset_index', resp = self.client.get(reverse('asset_index',
kwargs={'org': loc.org, kwargs={'org': loc.org,
'course': loc.course, 'course': loc.course,
'name': loc.name})) 'name': loc.name}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# go look at a subsection page # go look at a subsection page
subsection_location = loc.replace(category='sequential', name='test_sequence') subsection_location = loc.replace(category='sequential', name='test_sequence')
resp = self.client.get(reverse('edit_subsection', resp = self.client.get(reverse('edit_subsection',
kwargs={'location': subsection_location.url()})) kwargs={'location': subsection_location.url()}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# go look at the Edit page # go look at the Edit page
unit_location = loc.replace(category='vertical', name='test_vertical') unit_location = loc.replace(category='vertical', name='test_vertical')
resp = self.client.get(reverse('edit_unit', resp = self.client.get(reverse('edit_unit',
kwargs={'location': unit_location.url()})) kwargs={'location': unit_location.url()}))
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# delete a component # delete a component
del_loc = loc.replace(category='html', name='test_html') del_loc = loc.replace(category='html', name='test_html')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# delete a unit # delete a unit
del_loc = loc.replace(category='vertical', name='test_vertical') del_loc = loc.replace(category='vertical', name='test_vertical')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# delete a unit # delete a unit
del_loc = loc.replace(category='sequential', name='test_sequence') del_loc = loc.replace(category='sequential', name='test_sequence')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# delete a chapter # delete a chapter
del_loc = loc.replace(category='chapter', name='chapter_2') del_loc = loc.replace(category='chapter', name='chapter_2')
resp = self.client.post(reverse('delete_item'), resp = self.client.post(reverse('delete_item'),
json.dumps({'id': del_loc.url()}), "application/json") json.dumps({'id': del_loc.url()}), "application/json")
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
def test_import_into_new_course_id(self): def test_import_into_new_course_id(self):
module_store = modulestore('direct') module_store = modulestore('direct')
......
...@@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase): ...@@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase):
def test_get(self): def test_get(self):
resp = self.client.get(self.url) resp = self.client.get(self.url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
def test_delete(self): def test_delete(self):
resp = self.client.delete(self.url) resp = self.client.delete(self.url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
def test_post(self): def test_post(self):
grader = { grader = {
...@@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase): ...@@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase):
"weight": 17.3, "weight": 17.3,
} }
resp = self.client.post(self.url, grader) resp = self.client.post(self.url, grader)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
#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): ...@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
resp.content, resp.content,
"application/json" "application/json"
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
class TestCreateItem(CourseTestCase): class TestCreateItem(CourseTestCase):
......
...@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase):
def test_view_index(self): def test_view_index(self):
"Basic check that the textbook index page responds correctly" "Basic check that the textbook index page responds correctly"
resp = self.client.get(self.url) resp = self.client.get(self.url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# we don't have resp.context right now, # we don't have resp.context right now,
# due to bugs in our testing harness :( # due to bugs in our testing harness :(
if resp.context: if resp.context:
...@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertEqual(self.course.pdf_textbooks, obj) self.assertEqual(self.course.pdf_textbooks, obj)
...@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertEqual(content, obj) self.assertEqual(content, obj)
...@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
# reload course # reload course
store = get_modulestore(self.course.location) store = get_modulestore(self.course.location)
...@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase): ...@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH='XMLHttpRequest' HTTP_X_REQUESTED_WITH='XMLHttpRequest'
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
obj = json.loads(resp.content) obj = json.loads(resp.content)
self.assertIn("error", obj) self.assertIn("error", obj)
...@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase): ...@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase):
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest", HTTP_X_REQUESTED_WITH="XMLHttpRequest",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
self.assertNotIn("Location", resp) self.assertNotIn("Location", resp)
...@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase):
def test_get_1(self): def test_get_1(self):
"Get the first textbook" "Get the first textbook"
resp = self.client.get(self.url1) resp = self.client.get(self.url1)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content) compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook1) self.assertEqual(compare, self.textbook1)
def test_get_2(self): def test_get_2(self):
"Get the second textbook" "Get the second textbook"
resp = self.client.get(self.url2) resp = self.client.get(self.url2)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
compare = json.loads(resp.content) compare = json.loads(resp.content)
self.assertEqual(compare, self.textbook2) self.assertEqual(compare, self.textbook2)
...@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase):
def test_delete(self): def test_delete(self):
"Delete a textbook by ID" "Delete a textbook by ID"
resp = self.client.delete(self.url1) resp = self.client.delete(self.url1)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
course = self.store.get_item(self.course.location) course = self.store.get_item(self.course.location)
self.assertEqual(course.pdf_textbooks, [self.textbook2]) self.assertEqual(course.pdf_textbooks, [self.textbook2])
...@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase):
) )
self.assertEqual(resp.status_code, 201) self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(url) resp2 = self.client.get(url)
self.assert2XX(resp2.status_code) self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content) compare = json.loads(resp2.content)
self.assertEqual(compare, textbook) self.assertEqual(compare, textbook)
course = self.store.get_item(self.course.location) course = self.store.get_item(self.course.location)
...@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase): ...@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase):
) )
self.assertEqual(resp.status_code, 201) self.assertEqual(resp.status_code, 201)
resp2 = self.client.get(self.url2) resp2 = self.client.get(self.url2)
self.assert2XX(resp2.status_code) self.assertEqual(resp2.status_code, 200)
compare = json.loads(resp2.content) compare = json.loads(resp2.content)
self.assertEqual(compare, replacement) self.assertEqual(compare, replacement)
course = self.store.get_item(self.course.location) course = self.store.get_item(self.course.location)
......
...@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase): ...@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase):
def test_detail_inactive(self): def test_detail_inactive(self):
resp = self.client.get(self.inactive_detail_url) resp = self.client.get(self.inactive_detail_url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 200)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertFalse(result["active"]) self.assertFalse(result["active"])
def test_detail_invalid(self): def test_detail_invalid(self):
resp = self.client.get(self.invalid_detail_url) resp = self.client.get(self.invalid_detail_url)
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 404)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
...@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase): ...@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
data={"role": None}, data={"role": None},
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase): ...@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase): ...@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase): ...@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase): ...@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
self.assert_not_enrolled() self.assert_not_enrolled()
...@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase): ...@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
self.assert_not_enrolled() self.assert_not_enrolled()
...@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase): ...@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "staff"}, data={"role": "staff"},
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase): ...@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase): ...@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
ext_user = User.objects.get(email=self.ext_user.email) ext_user = User.objects.get(email=self.ext_user.email)
groups = [g.name for g in ext_user.groups.all()] groups = [g.name for g in ext_user.groups.all()]
...@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase): ...@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"}, data={"role": "instructor"},
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
...@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase): ...@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase):
data={"role": "instructor"}, data={"role": "instructor"},
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
...@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase): ...@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase):
}) })
resp = self.client.delete(self_url) resp = self.client.delete(self_url)
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
# reload user from DB # reload user from DB
user = User.objects.get(email=self.user.email) user = User.objects.get(email=self.user.email)
groups = [g.name for g in user.groups.all()] groups = [g.name for g in user.groups.all()]
...@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase): ...@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase):
self.ext_user.save() self.ext_user.save()
resp = self.client.delete(self.detail_url) resp = self.client.delete(self.detail_url)
self.assert4XX(resp.status_code) self.assertEqual(resp.status_code, 400)
result = json.loads(resp.content) result = json.loads(resp.content)
self.assertIn("error", result) self.assertIn("error", result)
# reload user from DB # reload user from DB
...@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase): ...@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase):
self.detail_url, self.detail_url,
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
self.assert_enrolled() self.assert_enrolled()
def test_staff_to_instructor_still_enrolled(self): def test_staff_to_instructor_still_enrolled(self):
...@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase): ...@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase):
content_type="application/json", content_type="application/json",
HTTP_ACCEPT="application/json", HTTP_ACCEPT="application/json",
) )
self.assert2XX(resp.status_code) self.assertEqual(resp.status_code, 204)
self.assert_enrolled() self.assert_enrolled()
def assert_not_enrolled(self): def assert_not_enrolled(self):
......
...@@ -8,6 +8,7 @@ so that we can run the lettuce acceptance tests. ...@@ -8,6 +8,7 @@ so that we can run the lettuce acceptance tests.
# pylint: disable=W0401, W0614 # pylint: disable=W0401, W0614
from .test import * from .test import *
from lms.envs.sauce import *
# You need to start the server in debug mode, # You need to start the server in debug mode,
# otherwise the browser will not render the pages correctly # otherwise the browser will not render the pages correctly
...@@ -17,7 +18,7 @@ DEBUG = True ...@@ -17,7 +18,7 @@ DEBUG = True
import logging import logging
logging.disable(logging.ERROR) logging.disable(logging.ERROR)
import os import os
import random from random import choice, randint
def seed(): def seed():
...@@ -75,7 +76,6 @@ DATABASES = { ...@@ -75,7 +76,6 @@ DATABASES = {
# Use the auto_auth workflow for creating users and logging them in # Use the auto_auth workflow for creating users and logging them in
MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
# HACK # HACK
# Setting this flag to false causes imports to not load correctly in the lettuce python files # 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 # We do not yet understand why this occurs. Setting this to true is a stopgap measure
...@@ -84,5 +84,5 @@ USE_I18N = True ...@@ -84,5 +84,5 @@ USE_I18N = True
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
INSTALLED_APPS += ('lettuce.django',) INSTALLED_APPS += ('lettuce.django',)
LETTUCE_APPS = ('contentstore',) LETTUCE_APPS = ('contentstore',)
LETTUCE_SERVER_PORT = random.randint(1024, 65535) LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
LETTUCE_BROWSER = 'chrome' LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome')
...@@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup ...@@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup
from course_groups.cohorts import (get_cohort, get_course_cohorts, from course_groups.cohorts import (get_cohort, get_course_cohorts,
is_commentable_cohorted, get_cohort_by_name) is_commentable_cohorted, get_cohort_by_name)
from xmodule.modulestore.django import modulestore, _MODULESTORES from xmodule.modulestore.django import modulestore, clear_existing_modulestores
from xmodule.modulestore.tests.django_utils import xml_store_config from xmodule.modulestore.tests.django_utils import mixed_store_config
# NOTE: running this with the lms.envs.test config works without # NOTE: running this with the lms.envs.test config works without
# manually overriding the modulestore. However, running with # manually overriding the modulestore. However, running with
# cms.envs.test doesn't. # cms.envs.test doesn't.
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'}
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING)
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class TestCohorts(django.test.TestCase): class TestCohorts(django.test.TestCase):
@staticmethod @staticmethod
...@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase): ...@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase):
""" """
Make sure that course is reloaded every time--clear out the modulestore. Make sure that course is reloaded every time--clear out the modulestore.
""" """
# don't like this, but don't know a better way to undo all changes made clear_existing_modulestores()
# to course. We don't have a course.clone() method.
_MODULESTORES.clear()
def test_get_cohort(self): def test_get_cohort(self):
""" """
......
...@@ -33,6 +33,7 @@ class CourseMode(models.Model): ...@@ -33,6 +33,7 @@ class CourseMode(models.Model):
currency = models.CharField(default="usd", max_length=8) currency = models.CharField(default="usd", max_length=8)
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd') DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd')
DEFAULT_MODE_SLUG = 'honor'
class Meta: class Meta:
""" meta attributes of this model """ """ meta attributes of this model """
...@@ -51,3 +52,18 @@ class CourseMode(models.Model): ...@@ -51,3 +52,18 @@ class CourseMode(models.Model):
if not modes: if not modes:
modes = [cls.DEFAULT_MODE] modes = [cls.DEFAULT_MODE]
return modes 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): ...@@ -60,3 +60,6 @@ class CourseModeModelTest(TestCase):
modes = CourseMode.modes_for_course(self.course_id) modes = CourseMode.modes_for_course(self.course_id)
self.assertEqual(modes, set_modes) 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 ...@@ -14,11 +14,9 @@ from django.contrib.auth.models import AnonymousUser, User
from django.utils.importlib import import_module from django.utils.importlib import import_module
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import editable_modulestore
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from external_auth.views import shib_login, course_specific_login, course_specific_register from external_auth.views import shib_login, course_specific_login, course_specific_register
...@@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment ...@@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment
from student.models import UserProfile, Registration, CourseEnrollment from student.models import UserProfile, Registration, CourseEnrollment
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
# Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider' # Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider'
# attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present # attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present
# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing # b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
...@@ -64,7 +64,7 @@ def gen_all_identities(): ...@@ -64,7 +64,7 @@ def gen_all_identities():
yield _build_identity_dict(mail, given_name, surname) yield _build_identity_dict(mail, given_name, surname)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache') @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
class ShibSPTest(ModuleStoreTestCase): class ShibSPTest(ModuleStoreTestCase):
""" """
Tests for the Shibboleth SP, which communicates via request.META Tests for the Shibboleth SP, which communicates via request.META
...@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase): ...@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase):
request_factory = RequestFactory() request_factory = RequestFactory()
def setUp(self): def setUp(self):
self.store = modulestore() self.store = editable_modulestore()
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True) @unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
def test_exception_shib_login(self): def test_exception_shib_login(self):
......
...@@ -90,7 +90,7 @@ def replace_course_urls(text, course_id): ...@@ -90,7 +90,7 @@ def replace_course_urls(text, course_id):
return re.sub(_url_replace_regex('/course/'), replace_course_url, text) 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, 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 (/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): ...@@ -100,6 +100,7 @@ def replace_static_urls(text, data_directory, course_id=None):
text: The source text to do the substitution in text: The source text to do the substitution in
data_directory: The directory in which course data is stored 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 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): def replace_static_url(match):
...@@ -116,7 +117,7 @@ def replace_static_urls(text, data_directory, course_id=None): ...@@ -116,7 +117,7 @@ def replace_static_urls(text, data_directory, course_id=None):
if settings.DEBUG and finders.find(rest, True): if settings.DEBUG and finders.find(rest, True):
return original return original
# if we're running with a MongoBacked store course_namespace is not None, then use studio style urls # 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 # 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) # 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): ...@@ -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) 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 # Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
else: else:
course_path = "/".join((data_directory, rest)) course_path = "/".join((static_asset_path or data_directory, rest))
try: try:
if staticfiles_storage.exists(rest): if staticfiles_storage.exists(rest):
...@@ -152,7 +153,7 @@ def replace_static_urls(text, data_directory, course_id=None): ...@@ -152,7 +153,7 @@ def replace_static_urls(text, data_directory, course_id=None):
return re.sub( 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, replace_static_url,
text text
) )
...@@ -827,9 +827,6 @@ class CourseEnrollment(models.Model): ...@@ -827,9 +827,6 @@ class CourseEnrollment(models.Model):
@classmethod @classmethod
def is_enrolled(cls, user, course_id): 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 Returns True if the user is enrolled in the course (the entry must exist
and it must have `is_active=True`). Otherwise, returns False. and it must have `is_active=True`). Otherwise, returns False.
......
...@@ -11,6 +11,10 @@ from logging import getLogger ...@@ -11,6 +11,10 @@ from logging import getLogger
from django.core.management import call_command from django.core.management import call_command
from django.conf import settings from django.conf import settings
from selenium.common.exceptions import WebDriverException 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 # Let the LMS and CMS do their one-time setup
# For example, setting up mongo caches # For example, setting up mongo caches
...@@ -42,11 +46,53 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...") ...@@ -42,11 +46,53 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...")
MAX_VALID_BROWSER_ATTEMPTS = 20 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 @before.harvest
def initial_setup(server): def initial_setup(server):
""" """
Launch the browser once before executing the tests. Launch the browser once before executing the tests.
""" """
world.absorb(settings.SAUCE.get('SAUCE_ENABLED'), 'SAUCE_ENABLED')
if not world.SAUCE_ENABLED:
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome') browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
# There is an issue with ChromeDriver2 r195627 on Ubuntu # There is an issue with ChromeDriver2 r195627 on Ubuntu
...@@ -55,8 +101,6 @@ def initial_setup(server): ...@@ -55,8 +101,6 @@ def initial_setup(server):
success = False success = False
num_attempts = 0 num_attempts = 0
while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS: while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
# Get a browser session
world.browser = Browser(browser_driver) world.browser = Browser(browser_driver)
# Try to visit the main page # Try to visit the main page
...@@ -77,9 +121,19 @@ def initial_setup(server): ...@@ -77,9 +121,19 @@ def initial_setup(server):
if not success: if not success:
raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver)) 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 @before.each_scenario
def reset_data(scenario): def reset_data(scenario):
...@@ -97,7 +151,6 @@ def clear_data(scenario): ...@@ -97,7 +151,6 @@ def clear_data(scenario):
world.spew('scenario_dict') world.spew('scenario_dict')
@after.each_scenario @after.each_scenario
def reset_databases(scenario): def reset_databases(scenario):
''' '''
...@@ -108,9 +161,10 @@ def reset_databases(scenario): ...@@ -108,9 +161,10 @@ def reset_databases(scenario):
mongo = MongoClient() mongo = MongoClient()
mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db']) mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db'])
_CONTENTSTORE.clear() _CONTENTSTORE.clear()
modulestore = xmodule.modulestore.django.modulestore()
modulestore = xmodule.modulestore.django.editable_modulestore()
modulestore.collection.drop() modulestore.collection.drop()
xmodule.modulestore.django._MODULESTORES.clear() xmodule.modulestore.django.clear_existing_modulestores()
# Uncomment below to trigger a screenshot on error # Uncomment below to trigger a screenshot on error
...@@ -128,4 +182,6 @@ def teardown_browser(total): ...@@ -128,4 +182,6 @@ def teardown_browser(total):
""" """
Quit the browser after executing the tests. 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() world.browser.quit()
...@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login ...@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login
from django.contrib.auth.middleware import AuthenticationMiddleware from django.contrib.auth.middleware import AuthenticationMiddleware
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import editable_modulestore
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from urllib import quote_plus from urllib import quote_plus
...@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False): ...@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False):
@world.absorb @world.absorb
def clear_courses(): def clear_courses():
# Flush and initialize the module store # Flush and initialize the module store
# It needs the templates because it creates new records
# by cloning from the template.
# Note that if your test module gets in some weird state # Note that if your test module gets in some weird state
# (though it shouldn't), do this manually # (though it shouldn't), do this manually
# from the bash shell to drop it: # from the bash shell to drop it:
# $ mongo test_xmodule --eval "db.dropDatabase()" # $ mongo test_xmodule --eval "db.dropDatabase()"
modulestore().collection.drop() editable_modulestore().collection.drop()
contentstore().fs_files.drop() contentstore().fs_files.drop()
...@@ -99,7 +99,7 @@ def i_am_logged_in_user(step): ...@@ -99,7 +99,7 @@ def i_am_logged_in_user(step):
@step('I am not logged in$') @step('I am not logged in$')
def i_am_not_logged_in(step): def i_am_not_logged_in(step):
world.browser.cookies.delete() world.visit('logout')
@step('I am staff for course "([^"]*)"$') @step('I am staff for course "([^"]*)"$')
...@@ -138,10 +138,13 @@ def should_have_link_with_path_and_text(step, path, text): ...@@ -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') @step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
def should_see_in_the_page(step, doesnt_appear, text): def should_see_in_the_page(step, doesnt_appear, text):
multiplier = 1
if world.SAUCE_ENABLED:
multiplier = 2
if doesnt_appear: 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: 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$') @step('I am logged in$')
...@@ -150,7 +153,7 @@ def i_am_logged_in(step): ...@@ -150,7 +153,7 @@ def i_am_logged_in(step):
world.log_in(username='robot', password='test') world.log_in(username='robot', password='test')
world.browser.visit(django_url('/')) world.browser.visit(django_url('/'))
# You should not see the login link # 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$') @step(u'I am an edX user$')
......
...@@ -76,7 +76,7 @@ def replace_course_urls(get_html, course_id): ...@@ -76,7 +76,7 @@ def replace_course_urls(get_html, course_id):
return _get_html 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 Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/... 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): ...@@ -85,7 +85,7 @@ def replace_static_urls(get_html, data_dir, course_id=None):
@wraps(get_html) @wraps(get_html)
def _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 return _get_html
......
...@@ -213,7 +213,7 @@ class CombinedOpenEndedFields(object): ...@@ -213,7 +213,7 @@ class CombinedOpenEndedFields(object):
help="The number of times the student can try to answer this problem.", help="The number of times the student can try to answer this problem.",
default=1, default=1,
scope=Scope.settings, scope=Scope.settings,
values={"min" : 1 } values={"min": 1 }
) )
accept_file_upload = Boolean( accept_file_upload = Boolean(
display_name="Allow File Uploads", display_name="Allow File Uploads",
...@@ -229,12 +229,10 @@ class CombinedOpenEndedFields(object): ...@@ -229,12 +229,10 @@ class CombinedOpenEndedFields(object):
) )
due = Date( due = Date(
help="Date that this problem is due by", help="Date that this problem is due by",
default=None,
scope=Scope.settings scope=Scope.settings
) )
graceperiod = Timedelta( graceperiod = Timedelta(
help="Amount of time after the due date that submissions will be accepted", help="Amount of time after the due date that submissions will be accepted",
default=None,
scope=Scope.settings scope=Scope.settings
) )
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
...@@ -244,7 +242,7 @@ class CombinedOpenEndedFields(object): ...@@ -244,7 +242,7 @@ class CombinedOpenEndedFields(object):
display_name="Problem Weight", 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.", 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, scope=Scope.settings,
values={"min" : 0 , "step": ".1"}, values={"min": 0, "step": ".1"},
default=1 default=1
) )
min_to_calibrate = Integer( min_to_calibrate = Integer(
...@@ -252,28 +250,28 @@ class CombinedOpenEndedFields(object): ...@@ -252,28 +250,28 @@ class CombinedOpenEndedFields(object):
help="The minimum number of calibration essays each student will need to complete for peer grading.", help="The minimum number of calibration essays each student will need to complete for peer grading.",
default=3, default=3,
scope=Scope.settings, scope=Scope.settings,
values={"min" : 1, "max" : 20, "step" : "1"} values={"min": 1, "max": 20, "step": "1"}
) )
max_to_calibrate = Integer( max_to_calibrate = Integer(
display_name="Maximum Peer Grading Calibrations", display_name="Maximum Peer Grading Calibrations",
help="The maximum number of calibration essays each student will need to complete for peer grading.", help="The maximum number of calibration essays each student will need to complete for peer grading.",
default=6, default=6,
scope=Scope.settings, scope=Scope.settings,
values={"min" : 1, "max" : 20, "step" : "1"} values={"min": 1, "max": 20, "step": "1"}
) )
peer_grader_count = Integer( peer_grader_count = Integer(
display_name="Peer Graders per Response", display_name="Peer Graders per Response",
help="The number of peers who will grade each submission.", help="The number of peers who will grade each submission.",
default=3, default=3,
scope=Scope.settings, scope=Scope.settings,
values={"min" : 1, "step" : "1", "max" : 5} values={"min": 1, "step": "1", "max": 5}
) )
required_peer_grading = Integer( required_peer_grading = Integer(
display_name="Required Peer Grading", display_name="Required Peer Grading",
help="The number of other students each student making a submission will have to grade.", help="The number of other students each student making a submission will have to grade.",
default=3, default=3,
scope=Scope.settings, scope=Scope.settings,
values={"min" : 1, "step" : "1", "max" : 5} values={"min": 1, "step": "1", "max": 5}
) )
markdown = String( markdown = String(
help="Markdown source of this module", help="Markdown source of this module",
......
...@@ -217,8 +217,8 @@ class CourseFields(object): ...@@ -217,8 +217,8 @@ class CourseFields(object):
has_children = True has_children = True
checklists = List(scope=Scope.settings, checklists = List(scope=Scope.settings,
default=[ default=[
{"short_description" : "Getting Started With Studio", {"short_description": "Getting Started With Studio",
"items" : [{"short_description": "Add Course Team Members", "items": [{"short_description": "Add Course Team Members",
"long_description": "Grant your collaborators permission to edit your course so you can work together.", "long_description": "Grant your collaborators permission to edit your course so you can work together.",
"is_checked": False, "is_checked": False,
"action_url": "ManageUsers", "action_url": "ManageUsers",
...@@ -241,10 +241,9 @@ class CourseFields(object): ...@@ -241,10 +241,9 @@ class CourseFields(object):
"is_checked": False, "is_checked": False,
"action_url": "", "action_url": "",
"action_text": "", "action_text": "",
"action_external": False}] "action_external": False}]},
}, {"short_description": "Draft a Rough Course Outline",
{"short_description" : "Draft a Rough Course Outline", "items": [{"short_description": "Create Your First Section and Subsection",
"items" : [{"short_description": "Create Your First Section and Subsection",
"long_description": "Use your course outline to build your first Section and Subsection.", "long_description": "Use your course outline to build your first Section and Subsection.",
"is_checked": False, "is_checked": False,
"action_url": "CourseOutline", "action_url": "CourseOutline",
...@@ -285,10 +284,9 @@ class CourseFields(object): ...@@ -285,10 +284,9 @@ class CourseFields(object):
"is_checked": False, "is_checked": False,
"action_url": "CourseOutline", "action_url": "CourseOutline",
"action_text": "Edit Course Outline", "action_text": "Edit Course Outline",
"action_external": False}] "action_external": False}]},
}, {"short_description": "Explore edX's Support Tools",
{"short_description" : "Explore edX's Support Tools", "items": [{"short_description": "Explore the Studio Help Forum",
"items" : [{"short_description": "Explore the Studio Help Forum",
"long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.", "long_description": "Access the Studio Help forum from the menu that appears when you click your user name in the top right corner of Studio.",
"is_checked": False, "is_checked": False,
"action_url": "http://help.edge.edx.org/", "action_url": "http://help.edge.edx.org/",
...@@ -305,10 +303,9 @@ class CourseFields(object): ...@@ -305,10 +303,9 @@ class CourseFields(object):
"is_checked": False, "is_checked": False,
"action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf", "action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf",
"action_text": "Download Documentation", "action_text": "Download Documentation",
"action_external": True}] "action_external": True}]},
}, {"short_description": "Draft Your Course About Page",
{"short_description" : "Draft Your Course About Page", "items": [{"short_description": "Draft a Course Description",
"items" : [{"short_description": "Draft a Course Description",
"long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.", "long_description": "Courses on edX have an About page that includes a course video, description, and more. Draft the text students will read before deciding to enroll in your course.",
"is_checked": False, "is_checked": False,
"action_url": "SettingsDetails", "action_url": "SettingsDetails",
...@@ -331,8 +328,7 @@ class CourseFields(object): ...@@ -331,8 +328,7 @@ class CourseFields(object):
"is_checked": False, "is_checked": False,
"action_url": "SettingsDetails", "action_url": "SettingsDetails",
"action_text": "Edit Course Schedule &amp; Details", "action_text": "Edit Course Schedule &amp; Details",
"action_external": False}] "action_external": False}]}
}
]) ])
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts') info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True) show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True)
......
...@@ -33,7 +33,9 @@ class HtmlFields(object): ...@@ -33,7 +33,9 @@ class HtmlFields(object):
class HtmlModule(HtmlFields, XModule): class HtmlModule(HtmlFields, XModule):
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'), js = {
'coffee': [
resource_string(__name__, 'js/src/javascript_loader.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/html/display.coffee') resource_string(__name__, 'js/src/html/display.coffee')
] ]
...@@ -118,8 +120,10 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -118,8 +120,10 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
# from .html # from .html
# 'filename' in html pointers is a relative path # 'filename' in html pointers is a relative path
# (not same as 'html/blah.html' when the pointer is in a directory itself) # (not same as 'html/blah.html' when the pointer is in a directory itself)
pointer_path = "{category}/{url_path}".format(category='html', pointer_path = "{category}/{url_path}".format(
url_path=name_to_pathname(location.name)) category='html',
url_path=name_to_pathname(location.name)
)
base = path(pointer_path).dirname() base = path(pointer_path).dirname()
# log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename)) # log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
filepath = "{base}/{name}.html".format(base=base, name=filename) filepath = "{base}/{name}.html".format(base=base, name=filename)
...@@ -164,19 +168,16 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -164,19 +168,16 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
# TODO (vshnayder): make export put things in the right places. # TODO (vshnayder): make export put things in the right places.
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
'''If the contents are valid xml, write them to filename.xml. Otherwise, ''' Write <html filename="" [meta-attrs="..."]> to filename.xml, and the html
write just <html filename="" [meta-attrs="..."]> to filename.xml, and the html
string to filename.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) pathname = name_to_pathname(self.url_name)
filepath = u'{category}/{pathname}.html'.format(category=self.category, filepath = u'{category}/{pathname}.html'.format(
pathname=pathname) category=self.category,
pathname=pathname
)
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True) resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as filestream: with resource_fs.open(filepath, 'w') as filestream:
...@@ -190,6 +191,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -190,6 +191,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
elt.set("filename", relname) elt.set("filename", relname)
return elt return elt
class AboutFields(object): class AboutFields(object):
display_name = String( display_name = String(
help="Display name for this module", help="Display name for this module",
...@@ -202,12 +204,14 @@ class AboutFields(object): ...@@ -202,12 +204,14 @@ class AboutFields(object):
scope=Scope.content scope=Scope.content
) )
class AboutModule(AboutFields, HtmlModule): class AboutModule(AboutFields, HtmlModule):
""" """
Overriding defaults but otherwise treated as HtmlModule. Overriding defaults but otherwise treated as HtmlModule.
""" """
pass pass
class AboutDescriptor(AboutFields, HtmlDescriptor): class AboutDescriptor(AboutFields, HtmlDescriptor):
""" """
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located 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): ...@@ -216,6 +220,7 @@ class AboutDescriptor(AboutFields, HtmlDescriptor):
template_dir_name = "about" template_dir_name = "about"
module_class = AboutModule module_class = AboutModule
class StaticTabFields(object): class StaticTabFields(object):
""" """
The overrides for Static Tabs The overrides for Static Tabs
...@@ -241,6 +246,7 @@ class StaticTabModule(StaticTabFields, HtmlModule): ...@@ -241,6 +246,7 @@ class StaticTabModule(StaticTabFields, HtmlModule):
""" """
pass pass
class StaticTabDescriptor(StaticTabFields, HtmlDescriptor): class StaticTabDescriptor(StaticTabFields, HtmlDescriptor):
""" """
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located 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="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"> <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> <h2>Problem 1</h2>
<div class="status-container"> <div class="status-container">
<h4>Status</h4> <h4>Status</h4>
<div class="status-elements"> <div class="status-elements">
<section id="combined-open-ended-status" class="combined-open-ended-status"> <section id="combined-open-ended-status" class="combined-open-ended-status">
<div class="statusitem" data-status-number="0"> <div class="statusitem" data-status-number="0">
Step 1 (Problem complete) : 1 / 1 Step 1 (Problem complete) : 1 / 1
<span class="correct" id="status"></span> <span class="correct" id="status"></span>
</div> </div>
<div class="statusitem statusitem-current" data-status-number="1"> <div class="statusitem statusitem-current" data-status-number="1">
Step 2 (Being scored) : None / 1 Step 2 (Being scored) : None / 1
<span class="grading" id="status"></span> <span class="grading" id="status"></span>
</div> </div>
</section> </section>
</div> </div>
</div> </div>
<div class="item-container"> <div class="item-container">
<h4>Problem</h4> <h4>Problem</h4>
<div class="problem-container"> <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="item">
<section id="openended_open_ended" class="open-ended-child" data-state="assessing" data-child-type="openended">
<div class="error">
</div>
<div class="prompt"> <div class="prompt">
Some prompt. Some prompt.
</div> </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> <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"> <div class="grader-status">
<span class="grading" id="status_open_ended">Submitted for grading.</span> <span class="grading" id="status_open_ended">Submitted for grading.</span>
</div> </div>
<input type="button" value="Submit assessment" class="submit-button" name="show" style="display: none;">
<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> <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> <span id="answer_open_ended"></span>
</section></div> </section>
</div> </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="Reset" class="reset-button" name="reset" style="display: none;">
</div>
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;"> <input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
</div> </div>
<a name="results"> <a name="results">
<div class="result-container"> <div class="result-container">
</div> </div>
</a></section><a name="results"> </a>
</section>
<a name="results">
</a></section><a name="results"> </a>
</section>
</a><div><a name="results"> <a name="results">
</a><a href="https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml">Edit</a> / </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', { <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', 'location': 'i4x://MITx/6.002x/combinedopenended/CombinedOE',
'xqa_key': 'KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A', 'xqa_key': 'KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A',
'category': 'CombinedOpenEndedModule', 'category': 'CombinedOpenEndedModule',
'user': 'blah' 'user': 'blah'
})" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA</a> })" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA</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> <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"> <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"> <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', -> describe 'CombinedOpenEnded', ->
beforeEach -> beforeEach ->
spyOn Logger, 'log' spyOn Logger, 'log'
...@@ -13,7 +40,7 @@ describe 'CombinedOpenEnded', -> ...@@ -13,7 +40,7 @@ describe 'CombinedOpenEnded', ->
@combined = new CombinedOpenEnded @element @combined = new CombinedOpenEnded @element
it 'set the element', -> it 'set the element', ->
expect(@combined.element).toEqual @element expect(@combined.el).toEqual @element
it 'get the correct values from data fields', -> 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' expect(@combined.ajax_url).toEqual '/courses/MITx/6.002x/2012_Fall/modx/i4x://MITx/6.002x/combinedopenended/CombinedOE'
...@@ -77,7 +104,7 @@ describe 'CombinedOpenEnded', -> ...@@ -77,7 +104,7 @@ describe 'CombinedOpenEnded', ->
@combined.child_state = 'done' @combined.child_state = 'done'
@combined.rebind() @combined.rebind()
expect(@combined.answer_area.attr("disabled")).toBe("disabled") expect(@combined.answer_area.attr("disabled")).toBe("disabled")
expect(@combined.next_problem).toHaveBeenCalled() expect(@combined.next_problem_button).toBe(":visible")
describe 'next_problem', -> describe 'next_problem', ->
beforeEach -> beforeEach ->
...@@ -109,3 +136,5 @@ describe 'CombinedOpenEnded', -> ...@@ -109,3 +136,5 @@ describe 'CombinedOpenEnded', ->
...@@ -3,10 +3,20 @@ ...@@ -3,10 +3,20 @@
# Can (and should be) expanded upon when our problem list # Can (and should be) expanded upon when our problem list
# becomes more sophisticated # becomes more sophisticated
class @PeerGrading 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) -> 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') @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') @ajax_url = @peer_grading_container.data('ajax-url')
if @use_single_location.toLowerCase() == "true" if @use_single_location.toLowerCase() == "true"
...@@ -14,23 +24,27 @@ class @PeerGrading ...@@ -14,23 +24,27 @@ class @PeerGrading
@activate_problem() @activate_problem()
else else
#Otherwise, activate the panel view. #Otherwise, activate the panel view.
@error_container = $('.error-container') @error_container = @$(@error_container_sel)
@error_container.toggle(not @error_container.is(':empty')) @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')) @message_container.toggle(not @message_container.is(':empty'))
@problem_button = $('.problem-button') @problem_button = @$(@problem_button_sel)
@problem_button.click @show_results @problem_button.click @show_results
@problem_list = $('.problem-list') @problem_list = @$(@problem_list_sel)
@construct_progress_bar() @construct_progress_bar()
# locally scoped jquery.
$: (selector) ->
$(selector, @el)
construct_progress_bar: () => construct_progress_bar: () =>
problems = @problem_list.find('tr').next() problems = @problem_list.find('tr').next()
problems.each( (index, element) => problems.each( (index, element) =>
problem = $(element) problem = $(element)
progress_bar = problem.find('.progress-bar') progress_bar = problem.find(@progress_bar_sel)
bar_value = parseInt(problem.data('graded')) bar_value = parseInt(problem.data('graded'))
bar_max = parseInt(problem.data('required')) + bar_value bar_max = parseInt(problem.data('required')) + bar_value
progress_bar.progressbar({value: bar_value, max: bar_max}) progress_bar.progressbar({value: bar_value, max: bar_max})
...@@ -43,10 +57,10 @@ class @PeerGrading ...@@ -43,10 +57,10 @@ class @PeerGrading
if response.success if response.success
@peer_grading_outer_container.after(response.html).remove() @peer_grading_outer_container.after(response.html).remove()
backend = new PeerGradingProblemBackend(@ajax_url, false) backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend) new PeerGradingProblem(backend, @el)
else else
@gentle_alert response.error @gentle_alert response.error
activate_problem: () => activate_problem: () =>
backend = new PeerGradingProblemBackend(@ajax_url, false) backend = new PeerGradingProblemBackend(@ajax_url, false)
new PeerGradingProblem(backend) new PeerGradingProblem(backend, @el)
\ No newline at end of file \ No newline at end of file
...@@ -55,6 +55,7 @@ def modulestore(name='default'): ...@@ -55,6 +55,7 @@ def modulestore(name='default'):
return _MODULESTORES[name] return _MODULESTORES[name]
_loc_singleton = None _loc_singleton = None
def loc_mapper(): def loc_mapper():
""" """
...@@ -69,3 +70,42 @@ def loc_mapper(): ...@@ -69,3 +70,42 @@ def loc_mapper():
_loc_singleton = LocMapperStore(settings.modulestore_options) _loc_singleton = LocMapperStore(settings.modulestore_options)
return _loc_singleton 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 = ( ...@@ -9,7 +9,8 @@ INHERITABLE_METADATA = (
# intended to be set per-course, but can be overridden in for specific # intended to be set per-course, but can be overridden in for specific
# elements. Can be a float. # elements. Can be a float.
'days_early_for_beta', '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 uuid import uuid4
from django.test import TestCase from django.test import TestCase
from xmodule.modulestore.django import editable_modulestore, \
clear_existing_modulestores
from django.conf import settings
import xmodule.modulestore.django def mixed_store_config(data_dir, mappings):
from unittest.util import safe_repr """
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): def mongo_store_config(data_dir):
...@@ -27,6 +62,7 @@ def mongo_store_config(data_dir): ...@@ -27,6 +62,7 @@ def mongo_store_config(data_dir):
} }
} }
} }
store['direct'] = store['default'] store['direct'] = store['default']
return store return store
...@@ -45,23 +81,22 @@ def draft_mongo_store_config(data_dir): ...@@ -45,23 +81,22 @@ def draft_mongo_store_config(data_dir):
'render_template': 'mitxmako.shortcuts.render_to_string' 'render_template': 'mitxmako.shortcuts.render_to_string'
} }
return { store = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
'OPTIONS': modulestore_options 'OPTIONS': modulestore_options
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'OPTIONS': modulestore_options
} }
} }
store['direct'] = store['default']
return store
def xml_store_config(data_dir): def xml_store_config(data_dir):
""" """
Defines default module store using XMLModuleStore. Defines default module store using XMLModuleStore.
""" """
return { store = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'OPTIONS': { 'OPTIONS': {
...@@ -71,12 +106,48 @@ def xml_store_config(data_dir): ...@@ -71,12 +106,48 @@ def xml_store_config(data_dir):
} }
} }
store['direct'] = store['default']
return store
class ModuleStoreTestCase(TestCase): class ModuleStoreTestCase(TestCase):
""" Subclass for any test case that uses the mongodb """
module store. This populates a uniquely named modulestore Subclass for any test case that uses a ModuleStore.
collection with templates before running the TestCase Ensures that the ModuleStore is cleaned before/after each test.
and drops it they are finished. """
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 @staticmethod
def update_course(course, data): def update_course(course, data):
...@@ -89,107 +160,68 @@ class ModuleStoreTestCase(TestCase): ...@@ -89,107 +160,68 @@ class ModuleStoreTestCase(TestCase):
'data' is a dictionary with an entry for each CourseField we want to update. '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) store.update_metadata(course.location, data)
updated_course = store.get_instance(course.id, course.location) updated_course = store.get_instance(course.id, course.location)
return updated_course return updated_course
@staticmethod @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 # This will return the mongo-backed modulestore
# that is not a template # even if we're using a mixed modulestore
query = {"_id.course": {"$ne": "templates"}} store = editable_modulestore()
# Remove everything except templates if hasattr(store, 'collection'):
modulestore.collection.remove(query) store.collection.drop()
modulestore.collection.drop()
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
""" """
Flush the mongo store and set up templates. Delete the existing modulestores, causing them to be reloaded.
""" """
# Clear out any existing modulestores,
# Use a uuid to differentiate # which will cause them to be re-created
# the mongo collections on jenkins. # the next time they are accessed.
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE) clear_existing_modulestores()
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
TestCase.setUpClass() TestCase.setUpClass()
@classmethod @classmethod
def tearDownClass(cls): 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 # Clear out the existing modulestores,
modulestore = xmodule.modulestore.django.modulestore() # which will cause them to be re-created
modulestore.collection.drop() # 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() TestCase.tearDownClass()
# Restore the original modulestore settings
settings.MODULESTORE = cls.orig_modulestore
def _pre_setup(self): 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 # Flush the Mongo modulestore
ModuleStoreTestCase.flush_mongo_except_templates() ModuleStoreTestCase.drop_mongo_collection()
# Call superclass implementation # Call superclass implementation
super(ModuleStoreTestCase, self)._pre_setup() super(ModuleStoreTestCase, self)._pre_setup()
def _post_teardown(self): 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.drop_mongo_collection()
ModuleStoreTestCase.flush_mongo_except_templates()
# Call superclass implementation # Call superclass implementation
super(ModuleStoreTestCase, self)._post_teardown() 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 ...@@ -5,11 +5,12 @@ from uuid import uuid4
from pytz import UTC from pytz import UTC
from xmodule.modulestore import Location 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 xmodule.course_module import CourseDescriptor
from xblock.core import Scope from xblock.core import Scope
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
class XModuleCourseFactory(Factory): class XModuleCourseFactory(Factory):
""" """
Factory for XModule courses. Factory for XModule courses.
...@@ -25,10 +26,7 @@ class XModuleCourseFactory(Factory): ...@@ -25,10 +26,7 @@ class XModuleCourseFactory(Factory):
display_name = kwargs.pop('display_name', None) display_name = kwargs.pop('display_name', None)
location = Location('i4x', org, number, 'course', Location.clean(display_name)) location = Location('i4x', org, number, 'course', Location.clean(display_name))
try: store = editable_modulestore('direct')
store = modulestore('direct')
except KeyError:
store = modulestore()
# Write the data to the mongo datastore # Write the data to the mongo datastore
new_course = store.create_xmodule(location) new_course = store.create_xmodule(location)
...@@ -117,7 +115,7 @@ class XModuleItemFactory(Factory): ...@@ -117,7 +115,7 @@ class XModuleItemFactory(Factory):
if not isinstance(data, basestring): if not isinstance(data, basestring):
data.update(template.get('data')) data.update(template.get('data'))
store = modulestore('direct') store = editable_modulestore('direct')
# This code was based off that in cms/djangoapps/contentstore/views.py # This code was based off that in cms/djangoapps/contentstore/views.py
parent = store.get_item(parent_location) parent = store.get_item(parent_location)
......
...@@ -10,7 +10,9 @@ from xblock.runtime import KeyValueStore, InvalidScopeError ...@@ -10,7 +10,9 @@ from xblock.runtime import KeyValueStore, InvalidScopeError
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore 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 from xmodule.modulestore.tests.test_modulestore import check_path_to_location
...@@ -35,7 +37,7 @@ class TestMongoModuleStore(object): ...@@ -35,7 +37,7 @@ class TestMongoModuleStore(object):
# is ok only as long as none of the tests modify the db. # 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 # If (when!) that changes, need to either reload the db, or load
# once and copy over to a tmp db for each test. # 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 @classmethod
def teardownClass(cls): def teardownClass(cls):
...@@ -46,10 +48,28 @@ class TestMongoModuleStore(object): ...@@ -46,10 +48,28 @@ class TestMongoModuleStore(object):
def initdb(): def initdb():
# connect to the db # connect to the db
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS) 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) # Explicitly list the courses to load (don't want the big one)
courses = ['toy', 'simple'] courses = ['toy', 'simple', 'simple_with_draft']
import_from_xml(store, DATA_DIR, courses) import_from_xml(store, DATA_DIR, courses, draft_store=draft_store, static_content_store=content_store)
return 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 @staticmethod
def destroy_db(connection): def destroy_db(connection):
...@@ -77,10 +97,12 @@ class TestMongoModuleStore(object): ...@@ -77,10 +97,12 @@ class TestMongoModuleStore(object):
def test_get_courses(self): def test_get_courses(self):
'''Make sure the course objects loaded properly''' '''Make sure the course objects loaded properly'''
courses = self.store.get_courses() courses = self.store.get_courses()
assert_equals(len(courses), 2) assert_equals(len(courses), 4)
courses.sort(key=lambda c: c.id) courses.sort(key=lambda c: c.id)
assert_equals(courses[0].id, 'edX/simple/2012_Fall') 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): def test_loads(self):
assert_not_equals( assert_not_equals(
...@@ -112,6 +134,13 @@ class TestMongoModuleStore(object): ...@@ -112,6 +134,13 @@ class TestMongoModuleStore(object):
'''Make sure that path_to_location works''' '''Make sure that path_to_location works'''
check_path_to_location(self.store) 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): def test_get_courses_has_no_templates(self):
courses = self.store.get_courses() courses = self.store.get_courses()
for course in courses: for course in courses:
...@@ -129,7 +158,7 @@ class TestMongoModuleStore(object): ...@@ -129,7 +158,7 @@ class TestMongoModuleStore(object):
Assumes the information is desired for courses[1] ('toy' course). 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 # 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 # was set set for tabs that have a URL slug. 'Syllabus' and 'Resources' fall into that
......
...@@ -51,7 +51,10 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ ...@@ -51,7 +51,10 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
content.thumbnail_location = thumbnail_location content.thumbnail_location = thumbnail_location
#then commit the content #then commit the content
try:
static_content_store.save(content) static_content_store.save(content)
except Exception as err:
log.exception('Error importing {0}, error={1}'.format(fullname_with_subpath, err))
#store the remapping information which will be needed to subsitute in the module data #store the remapping information which will be needed to subsitute in the module data
remap_dict[fullname_with_subpath] = content_loc.name remap_dict[fullname_with_subpath] = content_loc.name
...@@ -64,7 +67,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_ ...@@ -64,7 +67,8 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
def import_from_xml(store, data_dir, course_dirs=None, def import_from_xml(store, data_dir, course_dirs=None,
default_class='xmodule.raw_module.RawDescriptor', default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True, static_content_store=None, target_location_namespace=None, load_error_modules=True, static_content_store=None, target_location_namespace=None,
verbose=False, draft_store=None): verbose=False, draft_store=None,
do_import_static=True):
""" """
Import the specified xml data_dir into the "store" modulestore, Import the specified xml data_dir into the "store" modulestore,
using org and course as the location org and course. using org and course as the location org and course.
...@@ -77,6 +81,10 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -77,6 +81,10 @@ def import_from_xml(store, data_dir, course_dirs=None,
expects a 'url_name' as an identifier to where things are on disk e.g. ../policies/<url_name>/policy.json as well as metadata keys in expects a 'url_name' as an identifier to where things are on disk e.g. ../policies/<url_name>/policy.json as well as metadata keys in
the policy.json. so we need to keep the original url_name during import the policy.json. so we need to keep the original url_name during import
do_import_static: if False, then static files are not imported into the static content store. This can be employed for courses which
have substantial unchanging static content, which is to inefficient to import every time the course is loaded.
Static content for some courses may also be served directly by nginx, instead of going through django.
""" """
xml_module_store = XMLModuleStore( xml_module_store = XMLModuleStore(
...@@ -116,8 +124,17 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -116,8 +124,17 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_data_path = path(data_dir) / module.data_dir course_data_path = path(data_dir) / module.data_dir
course_location = module.location course_location = module.location
log.debug('======> IMPORTING course to location {0}'.format(course_location))
module = remap_namespace(module, target_location_namespace) module = remap_namespace(module, target_location_namespace)
if not do_import_static:
module.lms.static_asset_path = module.data_dir # for old-style xblock where this was actually linked to kvs
module._model_data['static_asset_path'] = module.data_dir
log.debug('course static_asset_path={0}'.format(module.lms.static_asset_path))
log.debug('course data_dir={0}'.format(module.data_dir))
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS, # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that - # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
...@@ -129,18 +146,35 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -129,18 +146,35 @@ def import_from_xml(store, data_dir, course_dirs=None,
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
import_module(module, store, course_data_path, static_content_store, course_location, import_module(module, store, course_data_path, static_content_store, course_location,
target_location_namespace or course_location) target_location_namespace or course_location, do_import_static=do_import_static)
course_items.append(module) course_items.append(module)
# then import all the static content # then import all the static content
if static_content_store is not None: if static_content_store is not None and do_import_static:
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
# first pass to find everything in /static/ # first pass to find everything in /static/
import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store, import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store,
_namespace_rename, subpath='static', verbose=verbose) _namespace_rename, subpath='static', verbose=verbose)
elif verbose and not do_import_static:
log.debug('Skipping import of static content, since do_import_static={0}'.format(do_import_static))
# no matter what do_import_static is, import "static_import" directory
# This is needed because the "about" pages (eg "overview") are loaded via load_extra_content, and
# do not inherit the lms metadata from the course module, and thus do not get "static_content_store"
# properly defined. Static content referenced in those extra pages thus need to come through the
# c4x:// contentstore, unfortunately. Tell users to copy that content into the "static_import" subdir.
simport = 'static_import'
if os.path.exists(course_data_path / simport):
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store,
_namespace_rename, subpath=simport, verbose=verbose)
# finally loop through all the modules # finally loop through all the modules
for module in xml_module_store.modules[course_id].itervalues(): for module in xml_module_store.modules[course_id].itervalues():
if module.category == 'course': if module.category == 'course':
...@@ -156,7 +190,8 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -156,7 +190,8 @@ def import_from_xml(store, data_dir, course_dirs=None,
log.debug('importing module location {0}'.format(module.location)) log.debug('importing module location {0}'.format(module.location))
import_module(module, store, course_data_path, static_content_store, course_location, import_module(module, store, course_data_path, static_content_store, course_location,
target_location_namespace if target_location_namespace else course_location) target_location_namespace if target_location_namespace else course_location,
do_import_static=do_import_static)
# now import any 'draft' items # now import any 'draft' items
if draft_store is not None: if draft_store is not None:
...@@ -176,7 +211,8 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -176,7 +211,8 @@ def import_from_xml(store, data_dir, course_dirs=None,
def import_module(module, store, course_data_path, static_content_store, def import_module(module, store, course_data_path, static_content_store,
source_course_location, dest_course_location, allow_not_found=False): source_course_location, dest_course_location, allow_not_found=False,
do_import_static=True):
logging.debug('processing import of module {0}...'.format(module.location.url())) logging.debug('processing import of module {0}...'.format(module.location.url()))
...@@ -196,7 +232,7 @@ def import_module(module, store, course_data_path, static_content_store, ...@@ -196,7 +232,7 @@ def import_module(module, store, course_data_path, static_content_store,
else: else:
module_data = content module_data = content
if isinstance(module_data, basestring): if isinstance(module_data, basestring) and do_import_static:
# we want to convert all 'non-portable' links in the module_data (if it is a string) to # we want to convert all 'non-portable' links in the module_data (if it is a string) to
# portable strings (e.g. /static/) # portable strings (e.g. /static/)
module_data = rewrite_nonportable_content_links( module_data = rewrite_nonportable_content_links(
...@@ -212,6 +248,15 @@ def import_module(module, store, course_data_path, static_content_store, ...@@ -212,6 +248,15 @@ def import_module(module, store, course_data_path, static_content_store,
# NOTE: It's important to use own_metadata here to avoid writing # NOTE: It's important to use own_metadata here to avoid writing
# inherited metadata everywhere. # inherited metadata everywhere.
# remove any export/import only xml_attributes which are used to wire together draft imports
if hasattr(module, 'xml_attributes') and 'parent_sequential_url' in module.xml_attributes:
del module.xml_attributes['parent_sequential_url']
if hasattr(module, 'xml_attributes') and 'index_in_children_list' in module.xml_attributes:
del module.xml_attributes['index_in_children_list']
module.save()
store.update_metadata(module.location, dict(own_metadata(module))) store.update_metadata(module.location, dict(own_metadata(module)))
...@@ -281,7 +326,7 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path, ...@@ -281,7 +326,7 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path,
# this is to make sure private only verticals show up in the list of children since # this is to make sure private only verticals show up in the list of children since
# they would have been filtered out from the non-draft store export # they would have been filtered out from the non-draft store export
if module.location.category == 'vertical': if module.location.category == 'vertical':
module.location = module.location._replace(revision=None) non_draft_location = module.location._replace(revision=None)
sequential_url = module.xml_attributes['parent_sequential_url'] sequential_url = module.xml_attributes['parent_sequential_url']
index = int(module.xml_attributes['index_in_children_list']) index = int(module.xml_attributes['index_in_children_list'])
...@@ -291,15 +336,12 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path, ...@@ -291,15 +336,12 @@ def import_course_draft(xml_module_store, store, draft_store, course_data_path,
seq_location = seq_location._replace(org=target_location_namespace.org, seq_location = seq_location._replace(org=target_location_namespace.org,
course=target_location_namespace.course course=target_location_namespace.course
) )
sequential = store.get_item(seq_location) sequential = store.get_item(seq_location, depth=0)
if module.location.url() not in sequential.children: if non_draft_location.url() not in sequential.children:
sequential.children.insert(index, module.location.url()) sequential.children.insert(index, non_draft_location.url())
store.update_children(sequential.location, sequential.children) store.update_children(sequential.location, sequential.children)
del module.xml_attributes['parent_sequential_url']
del module.xml_attributes['index_in_children_list']
import_module(module, draft_store, course_data_path, static_content_store, import_module(module, draft_store, course_data_path, static_content_store,
source_location_namespace, target_location_namespace, allow_not_found=True) source_location_namespace, target_location_namespace, allow_not_found=True)
for child in module.get_children(): for child in module.get_children():
......
...@@ -206,20 +206,49 @@ class CombinedOpenEndedRubric(object): ...@@ -206,20 +206,49 @@ class CombinedOpenEndedRubric(object):
def render_combined_rubric(self, rubric_xml, scores, score_types, feedback_types): def render_combined_rubric(self, rubric_xml, scores, score_types, feedback_types):
success, score_tuples = CombinedOpenEndedRubric.reformat_scores_for_rendering(scores, score_types, success, score_tuples = CombinedOpenEndedRubric.reformat_scores_for_rendering(scores, score_types,
feedback_types) feedback_types)
#Get all the categories in the rubric
rubric_categories = self.extract_categories(rubric_xml) 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) 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) max_score = max(max_scores)
for i in xrange(0, len(rubric_categories)): #Loop through each category
category = rubric_categories[i] for i, category in enumerate(rubric_categories):
for j in xrange(0, len(category['options'])): #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'] = [] rubric_categories[i]['options'][j]['grader_types'] = []
for tuple in score_tuples: #Score tuples are a flat data structure with (category, option, grader_type_list) for selected graders
if tuple[1] == i and tuple[2] == j: for tup in score_tuples:
for grader_type in tuple[3]: 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) 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), html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
{'categories': rubric_categories, {'categories': rubric_categories,
'max_scores': max_scores,
'correct' : correct,
'has_score': True, 'has_score': True,
'view_only': True, 'view_only': True,
'max_score': max_score, 'max_score': max_score,
......
...@@ -11,6 +11,9 @@ log = logging.getLogger(__name__) ...@@ -11,6 +11,9 @@ log = logging.getLogger(__name__)
class GradingServiceError(Exception): class GradingServiceError(Exception):
"""
Exception for grading service. Shown when Open Response Assessment servers cannot be reached.
"""
pass pass
...@@ -62,7 +65,6 @@ class GradingService(object): ...@@ -62,7 +65,6 @@ class GradingService(object):
""" """
Make a get request to the grading controller Make a get request to the grading controller
""" """
log.debug(params)
op = lambda: self.session.get(url, op = lambda: self.session.get(url,
allow_redirects=allow_redirects, allow_redirects=allow_redirects,
params=params) params=params)
......
...@@ -641,6 +641,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -641,6 +641,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
""" """
# Once we close the problem, we should not allow students # Once we close the problem, we should not allow students
# to save answers # to save answers
error_message = ""
closed, msg = self.check_if_closed() closed, msg = self.check_if_closed()
if closed: if closed:
return msg return msg
...@@ -650,25 +651,19 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -650,25 +651,19 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
# add new history element with answer and empty score and hint. # add new history element with answer and empty score and hint.
success, data = self.append_image_to_student_answer(data) success, data = self.append_image_to_student_answer(data)
error_message = ""
if success: 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']) data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer'])
self.new_history_entry(data['student_answer']) self.new_history_entry(data['student_answer'])
self.send_to_grader(data['student_answer'], system) self.send_to_grader(data['student_answer'], system)
self.change_state(self.ASSESSING) self.change_state(self.ASSESSING)
else: else:
# Error message already defined
success = False
else:
# This is a student_facing_error # 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." 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."
return { return {
'success': success, 'success': success,
'error': error_message, 'error': error_message,
'student_response': data['student_answer'] 'student_response': data['student_answer'].replace("\n","<br/>")
} }
def update_score(self, data, system): def update_score(self, data, system):
...@@ -699,12 +694,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -699,12 +694,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
score = self.latest_score() score = self.latest_score()
correct = 'correct' if self.is_submission_correct(score) else 'incorrect' correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
if self.child_state == self.ASSESSING: 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: else:
post_assessment = "" post_assessment = ""
correct = "" correct = ""
previous_answer = self.initial_display previous_answer = ""
previous_answer = previous_answer.replace("\n","<br/>")
context = { context = {
'prompt': self.child_prompt, 'prompt': self.child_prompt,
'previous_answer': previous_answer, 'previous_answer': previous_answer,
......
...@@ -179,10 +179,11 @@ class OpenEndedChild(object): ...@@ -179,10 +179,11 @@ class OpenEndedChild(object):
answer = autolink_html(answer) answer = autolink_html(answer)
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True, 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, 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 = cleaner.clean_html(answer)
clean_html = re.sub(r'</p>$', '', re.sub(r'^<p>', '', clean_html)) 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 clean_html = answer
return clean_html return clean_html
...@@ -332,7 +333,7 @@ class OpenEndedChild(object): ...@@ -332,7 +333,7 @@ class OpenEndedChild(object):
try: try:
image_data.seek(0) image_data.seek(0)
image_ok = open_ended_image_submission.run_image_tests(image_data) image_ok = open_ended_image_submission.run_image_tests(image_data)
except: except Exception:
log.exception("Could not create image and check it.") log.exception("Could not create image and check it.")
if image_ok: if image_ok:
...@@ -345,7 +346,7 @@ class OpenEndedChild(object): ...@@ -345,7 +346,7 @@ class OpenEndedChild(object):
success, s3_public_url = open_ended_image_submission.upload_to_s3( success, s3_public_url = open_ended_image_submission.upload_to_s3(
image_data, image_key, self.s3_interface image_data, image_key, self.s3_interface
) )
except: except Exception:
log.exception("Could not upload image to S3.") log.exception("Could not upload image to S3.")
return success, image_ok, s3_public_url return success, image_ok, s3_public_url
...@@ -434,38 +435,6 @@ class OpenEndedChild(object): ...@@ -434,38 +435,6 @@ class OpenEndedChild(object):
return success, string 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): def get_eta(self):
if self.controller_qs: if self.controller_qs:
response = self.controller_qs.check_for_eta(self.location_string) response = self.controller_qs.check_for_eta(self.location_string)
......
...@@ -124,4 +124,4 @@ class MockPeerGradingService(object): ...@@ -124,4 +124,4 @@ class MockPeerGradingService(object):
]} ]}
def get_data_for_location(self, problem_location, student_id): 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): ...@@ -61,6 +61,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
else: else:
previous_answer = '' previous_answer = ''
previous_answer = previous_answer.replace("\n","<br/>")
context = { context = {
'prompt': self.child_prompt, 'prompt': self.child_prompt,
'previous_answer': previous_answer, 'previous_answer': previous_answer,
...@@ -184,15 +185,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -184,15 +185,10 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
# add new history element with answer and empty score and hint. # add new history element with answer and empty score and hint.
success, data = self.append_image_to_student_answer(data) success, data = self.append_image_to_student_answer(data)
if success: 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']) data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer'])
self.new_history_entry(data['student_answer']) self.new_history_entry(data['student_answer'])
self.change_state(self.ASSESSING) self.change_state(self.ASSESSING)
else: else:
# Error message already defined
success = False
else:
# This is a student_facing_error # 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." 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): ...@@ -200,7 +196,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'success': success, 'success': success,
'rubric_html': self.get_rubric_html(system), 'rubric_html': self.get_rubric_html(system),
'error': error_message, 'error': error_message,
'student_response': data['student_answer'], 'student_response': data['student_answer'].replace("\n","<br/>")
} }
def save_assessment(self, data, _system): def save_assessment(self, data, _system):
...@@ -272,8 +268,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -272,8 +268,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
try: try:
rubric_scores = json.loads(latest_post_assessment) rubric_scores = json.loads(latest_post_assessment)
except: 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 = [] rubric_scores = []
return [rubric_scores] return [rubric_scores]
......
...@@ -46,7 +46,6 @@ class PeerGradingFields(object): ...@@ -46,7 +46,6 @@ class PeerGradingFields(object):
) )
due = Date( due = Date(
help="Due date that should be displayed.", help="Due date that should be displayed.",
default=None,
scope=Scope.settings) scope=Scope.settings)
graceperiod = Timedelta( graceperiod = Timedelta(
help="Amount of grace to give on the due date.", help="Amount of grace to give on the due date.",
...@@ -189,9 +188,8 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -189,9 +188,8 @@ class PeerGradingModule(PeerGradingFields, XModule):
return json.dumps(d, cls=ComplexEncoder) 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 student_id = self.system.anonymous_student_id
location = self.link_to_location
success = False success = False
response = {} response = {}
...@@ -229,7 +227,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -229,7 +227,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
count_graded = self.student_data_for_location['count_graded'] count_graded = self.student_data_for_location['count_graded']
count_required = self.student_data_for_location['count_required'] count_required = self.student_data_for_location['count_required']
except: except:
success, response = self.query_data_for_location() success, response = self.query_data_for_location(self.location)
if not success: if not success:
log.exception( log.exception(
"No instance data found and could not get data from controller for loc {0} student {1}".format( "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): ...@@ -312,17 +310,26 @@ class PeerGradingModule(PeerGradingFields, XModule):
error: if there was an error in the submission, this is the error message 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']) required = ['location', 'submission_id', 'submission_key', 'score', 'feedback', 'submission_flagged', 'answer_unknown']
success, message = self._check_required(data, required) 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: if not success:
return self._err_response(message) return self._err_response(message)
data_dict = {k:data.get(k) for k in required} data_dict = {k:data.get(k) for k in required}
if 'rubric_scores[]' in required:
data_dict['rubric_scores'] = data.getlist('rubric_scores[]') data_dict['rubric_scores'] = data.getlist('rubric_scores[]')
data_dict['grader_id'] = self.system.anonymous_student_id data_dict['grader_id'] = self.system.anonymous_student_id
try: try:
response = self.peer_gs.save_grade(**data_dict) 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 return response
except GradingServiceError: except GradingServiceError:
# This is a dev_facing_error # This is a dev_facing_error
...@@ -502,7 +509,7 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -502,7 +509,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
error_text = "Could not get list of problems to peer grade. Please notify course staff." error_text = "Could not get list of problems to peer grade. Please notify course staff."
log.error(error_text) log.error(error_text)
success = False success = False
except: except Exception:
log.exception("Could not contact peer grading service.") log.exception("Could not contact peer grading service.")
success = False success = False
...@@ -513,20 +520,24 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -513,20 +520,24 @@ class PeerGradingModule(PeerGradingFields, XModule):
''' '''
try: try:
return modulestore().get_instance(self.system.course_id, location) return modulestore().get_instance(self.system.course_id, location)
except: except Exception:
# the linked problem doesn't exist # the linked problem doesn't exist
log.error("Problem {0} does not exist in this course".format(location)) log.error("Problem {0} does not exist in this course".format(location))
raise raise
good_problem_list = []
for problem in problem_list: for problem in problem_list:
problem_location = problem['location'] problem_location = problem['location']
try:
descriptor = _find_corresponding_module_for_location(problem_location) descriptor = _find_corresponding_module_for_location(problem_location)
except Exception:
continue
if descriptor: if descriptor:
problem['due'] = descriptor.lms.due problem['due'] = descriptor.lms.due
grace_period = descriptor.lms.graceperiod grace_period = descriptor.lms.graceperiod
try: try:
problem_timeinfo = TimeInfo(problem['due'], grace_period) 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)) log.error("Malformed due date or grace period string for location {0}".format(problem_location))
raise raise
if self._closed(problem_timeinfo): if self._closed(problem_timeinfo):
...@@ -537,13 +548,14 @@ class PeerGradingModule(PeerGradingFields, XModule): ...@@ -537,13 +548,14 @@ class PeerGradingModule(PeerGradingFields, XModule):
# if we can't find the due date, assume that it doesn't have one # if we can't find the due date, assume that it doesn't have one
problem['due'] = None problem['due'] = None
problem['closed'] = False problem['closed'] = False
good_problem_list.append(problem)
ajax_url = self.ajax_url ajax_url = self.ajax_url
html = self.system.render_template('peer_grading/peer_grading.html', { html = self.system.render_template('peer_grading/peer_grading.html', {
'course_id': self.system.course_id, 'course_id': self.system.course_id,
'ajax_url': ajax_url, 'ajax_url': ajax_url,
'success': success, 'success': success,
'problem_list': problem_list, 'problem_list': good_problem_list,
'error_text': error_text, 'error_text': error_text,
# Checked above # Checked above
'staff_access': False, 'staff_access': False,
......
...@@ -73,6 +73,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -73,6 +73,7 @@ class OpenEndedChildTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_system = get_test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None
self.openendedchild = OpenEndedChild(self.test_system, self.location, self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -203,7 +204,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -203,7 +204,7 @@ class OpenEndedModuleTest(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_system = get_test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message") self.mock_xqueue.send_to_queue.return_value = (None, "Message")
...@@ -410,6 +411,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -410,6 +411,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2) full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
descriptor = Mock(data=full_definition) descriptor = Mock(data=full_definition)
test_system = get_test_system() test_system = get_test_system()
test_system.open_ended_grading_interface = None
combinedoe_container = CombinedOpenEndedModule( combinedoe_container = CombinedOpenEndedModule(
test_system, test_system,
descriptor, descriptor,
...@@ -536,6 +538,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -536,6 +538,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
def setUp(self): def setUp(self):
self.test_system = get_test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None
self.test_system.xqueue['interface'] = Mock( self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"]) send_to_queue=Mock(side_effect=[1, "queued"])
) )
...@@ -569,9 +572,9 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -569,9 +572,9 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module = self.get_module_from_location(self.problem_location, COURSE) module = self.get_module_from_location(self.problem_location, COURSE)
#Simulate a student saving an answer #Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer": self.answer}) html = module.handle_ajax("get_html", {})
status = module.handle_ajax("get_status", {}) module.handle_ajax("save_answer", {"student_answer": self.answer, "can_upload_files" : False, "student_file" : None})
self.assertTrue(isinstance(status, basestring)) html = module.handle_ajax("get_html", {})
#Mock a student submitting an assessment #Mock a student submitting an assessment
assessment_dict = MockQueryDict() assessment_dict = MockQueryDict()
...@@ -579,8 +582,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -579,8 +582,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("save_assessment", assessment_dict) module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0]) task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
status = module.handle_ajax("get_status", {}) rubric = module.handle_ajax("get_combined_rubric", {})
self.assertTrue(isinstance(status, basestring))
#Move to the next step in the problem #Move to the next step in the problem
module.handle_ajax("next_problem", {}) module.handle_ajax("next_problem", {})
...@@ -617,7 +619,6 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -617,7 +619,6 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("save_assessment", assessment_dict) module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0]) task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) 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 #Move to the next step in the problem
try: try:
...@@ -660,15 +661,11 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore): ...@@ -660,15 +661,11 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
#Get html and other data client will request #Get html and other data client will request
module.get_html() 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", {}) module.handle_ajax("skip_post_assessment", {})
self.assertTrue(isinstance(legend, basestring))
#Get all results #Get all results
module.handle_ajax("get_results", {}) module.handle_ajax("get_combined_rubric", {})
#reset the problem #reset the problem
module.handle_ajax("reset", {}) module.handle_ajax("reset", {})
...@@ -686,6 +683,7 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore): ...@@ -686,6 +683,7 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
def setUp(self): def setUp(self):
self.test_system = get_test_system() self.test_system = get_test_system()
self.test_system.open_ended_grading_interface = None
self.test_system.xqueue['interface'] = Mock( self.test_system.xqueue['interface'] = Mock(
send_to_queue=Mock(side_effect=[1, "queued"]) send_to_queue=Mock(side_effect=[1, "queued"])
) )
...@@ -702,8 +700,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore): ...@@ -702,8 +700,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
#Simulate a student saving an answer #Simulate a student saving an answer
module.handle_ajax("save_answer", {"student_answer": self.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 #Mock a student submitting an assessment
assessment_dict = MockQueryDict() assessment_dict = MockQueryDict()
...@@ -711,8 +707,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore): ...@@ -711,8 +707,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
module.handle_ajax("save_assessment", assessment_dict) module.handle_ajax("save_assessment", assessment_dict)
task_one_json = json.loads(module.task_states[0]) task_one_json = json.loads(module.task_states[0])
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment) 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 #Move to the next step in the problem
module.handle_ajax("next_problem", {}) module.handle_ajax("next_problem", {})
......
...@@ -89,12 +89,6 @@ class RoundTripTestCase(unittest.TestCase): ...@@ -89,12 +89,6 @@ class RoundTripTestCase(unittest.TestCase):
print("Checking module equality") print("Checking module equality")
for location in initial_import.modules[course_id].keys(): for location in initial_import.modules[course_id].keys():
print("Checking", location) 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], self.assertEquals(initial_import.modules[course_id][location],
second_import.modules[course_id][location]) second_import.modules[course_id][location])
......
...@@ -61,7 +61,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore): ...@@ -61,7 +61,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
Try getting data from the external grading service Try getting data from the external grading service
@return: @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) self.assertEqual(success, True)
def test_get_score(self): 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 @@ ...@@ -8,6 +8,8 @@
<html url_name="nonportable"/> <html url_name="nonportable"/>
<html url_name="nonportable_link"/> <html url_name="nonportable_link"/>
<html url_name="badlink"/> <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"/> <video url_name="Video_Resources" youtube_id_1_0="1bK-WdDi6Qw" display_name="Video Resources"/>
</videosequence> </videosequence>
<video url_name="Welcome" youtube_id_1_0="p2Q6BrNhdh8" display_name="Welcome"/> <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: ...@@ -136,6 +136,25 @@ To run a single nose test:
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify 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) 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 ...@@ -30,10 +30,22 @@ TESTS_FAILED=0
# /usr/bin/Xvfb :1 -screen 0 1024x268x24 # /usr/bin/Xvfb :1 -screen 0 1024x268x24
# This allows us to run Chrome without a display # This allows us to run Chrome without a display
export DISPLAY=:1 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 # Run the lms and cms acceptance tests
# (the -v flag turns off color in the output) # (the -v flag turns off color in the output)
rake test_acceptance_lms["-v 3"] || TESTS_FAILED=1 rake test_acceptance_lms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
rake test_acceptance_cms["-v 3"] || TESTS_FAILED=1 rake test_acceptance_cms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
[ $TESTS_FAILED == '0' ] [ $TESTS_FAILED == '0' ]
...@@ -3,21 +3,18 @@ from django.test.utils import override_settings ...@@ -3,21 +3,18 @@ from django.test.utils import override_settings
import xmodule.modulestore.django 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 from xmodule.modulestore.django import modulestore
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
class WikiRedirectTestCase(LoginEnrollmentTestCase): class WikiRedirectTestCase(LoginEnrollmentTestCase):
def setUp(self):
xmodule.modulestore.django._MODULESTORES = {}
courses = modulestore().get_courses()
def find_course(name): def setUp(self):
"""Assumes the course is present"""
return [c for c in courses if c.location.course == name][0]
self.toy = find_course("toy") # Load the toy course
self.toy = modulestore().get_course('edX/toy/2012_Fall')
# Create two accounts # Create two accounts
self.student = 'view@test.com' self.student = 'view@test.com'
......
...@@ -81,8 +81,8 @@ def get_opt_course_with_access(user, course_id, action): ...@@ -81,8 +81,8 @@ def get_opt_course_with_access(user, course_id, action):
def course_image_url(course): def course_image_url(course):
"""Try to look up the image url for the course. If it's not found, """Try to look up the image url for the course. If it's not found,
log an error and return the dead link""" log an error and return the dead link"""
if modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE: if course.lms.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
return '/static/' + course.data_dir + "/images/course_image.jpg" return '/static/' + (course.lms.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg"
else: else:
loc = course.location._replace(tag='c4x', category='asset', name=course.course_image) loc = course.location._replace(tag='c4x', category='asset', name=course.course_image)
_path = StaticContent.get_url_path_from_location(loc) _path = StaticContent.get_url_path_from_location(loc)
...@@ -156,7 +156,8 @@ def get_course_about_section(course, section_key): ...@@ -156,7 +156,8 @@ def get_course_about_section(course, section_key):
model_data_cache, model_data_cache,
course.id, course.id,
not_found_ok=True, not_found_ok=True,
wrap_xmodule_display=False wrap_xmodule_display=False,
static_asset_path=course.lms.static_asset_path
) )
html = '' html = ''
...@@ -204,7 +205,8 @@ def get_course_info_section(request, course, section_key): ...@@ -204,7 +205,8 @@ def get_course_info_section(request, course, section_key):
loc, loc,
model_data_cache, model_data_cache,
course.id, course.id,
wrap_xmodule_display=False wrap_xmodule_display=False,
static_asset_path=course.lms.static_asset_path
) )
html = '' html = ''
...@@ -242,7 +244,8 @@ def get_course_syllabus_section(course, section_key): ...@@ -242,7 +244,8 @@ def get_course_syllabus_section(course, section_key):
return replace_static_urls( return replace_static_urls(
htmlFile.read().decode('utf-8'), htmlFile.read().decode('utf-8'),
getattr(course, 'data_dir', None), 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: except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format( log.exception("Missing syllabus section {key} in course {url}".format(
......
...@@ -11,7 +11,8 @@ Feature: Login in as a registered user ...@@ -11,7 +11,8 @@ Feature: Login in as a registered user
And I submit my credentials on the login form And I submit my credentials on the login form
Then I should see the login error message "This account has not been activated" 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 Scenario: Login to an activated account
Given I am an edX user Given I am an edX user
And I am an activated user And I am an activated user
......
...@@ -3,7 +3,8 @@ Feature: Sign in ...@@ -3,7 +3,8 @@ Feature: Sign in
As a new user As a new user
I want to signup for a student account 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 Scenario: Sign up from the homepage
Given I visit the homepage Given I visit the homepage
When I click the link with the text "Register Now" When I click the link with the text "Register Now"
......
...@@ -11,6 +11,8 @@ Feature: Video component ...@@ -11,6 +11,8 @@ Feature: Video component
Given the course has a Video component in Youtube mode Given the course has a Video component in Youtube mode
Then when I view the video it has rendered 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 Scenario: Autoplay is enabled in LMS for a Video component
Given the course has a Video component in HTML5 mode Given the course has a Video component in HTML5 mode
Then when I view the video it has autoplay enabled 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_ ...@@ -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, def get_module(user, request, location, model_data_cache, course_id,
position=None, not_found_ok=False, wrap_xmodule_display=True, 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, Get an instance of the xmodule class identified by location,
setting the state based on an existing StudentModule, or creating one if none 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, ...@@ -141,6 +142,10 @@ def get_module(user, request, location, model_data_cache, course_id,
position within module position within module
- depth : number of levels of descendents to cache when loading this module. - depth : number of levels of descendents to cache when loading this module.
None means cache all descendents 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 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 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, ...@@ -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, return get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
position=position, position=position,
wrap_xmodule_display=wrap_xmodule_display, 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: except ItemNotFoundError:
if not not_found_ok: if not not_found_ok:
log.exception("Error in get_module") log.exception("Error in get_module")
...@@ -179,7 +185,8 @@ def get_xqueue_callback_url_prefix(request): ...@@ -179,7 +185,8 @@ def get_xqueue_callback_url_prefix(request):
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id, 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. 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 ...@@ -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, return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, xqueue_callback_url_prefix, 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, def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, xqueue_callback_url_prefix, 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. Actually implement get_module, without requiring a request.
...@@ -282,7 +291,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -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.... # 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, return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
track_function, make_xqueue_callback, 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): def xblock_model_data(descriptor):
return DbModel( return DbModel(
...@@ -349,6 +359,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -349,6 +359,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
static_replace.replace_static_urls, static_replace.replace_static_urls,
data_directory=getattr(descriptor, 'data_dir', None), data_directory=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,
), ),
replace_course_urls=partial( replace_course_urls=partial(
static_replace.replace_course_urls, static_replace.replace_course_urls,
...@@ -407,7 +418,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours ...@@ -407,7 +418,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
_get_html, _get_html,
getattr(descriptor, 'data_dir', None), 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 # 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): ...@@ -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']) 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, model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course.id,
request.user, modulestore().get_instance(course.id, loc), depth=0) 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)) logging.debug('course_module = {0}'.format(tab_module))
......
...@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse ...@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse
from django.test.client import Client from django.test.client import Client
from student.tests.factories import UserFactory, CourseEnrollmentFactory 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.tests import get_test_system
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory ...@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase 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): class BaseTestXmodule(ModuleStoreTestCase):
"""Base class for testing Xmodules with mongo store. """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 from django.conf import settings
...@@ -6,3 +12,15 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT ...@@ -6,3 +12,15 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
TEST_DATA_MONGO_MODULESTORE = mongo_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) 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