Merge branch 'master' into lapentab/fix_network_tests
Conflicts: lms/djangoapps/courseware/tests/tests.py
This commit is contained in:
@@ -2,6 +2,7 @@ Feature: Advanced (manual) course policy
|
||||
In order to specify course policy settings for which no custom user interface exists
|
||||
I want to be able to manually enter JSON key /value pairs
|
||||
|
||||
|
||||
Scenario: A course author sees default advanced settings
|
||||
Given I have opened a new course in Studio
|
||||
When I select the Advanced Settings
|
||||
@@ -11,6 +12,8 @@ Feature: Advanced (manual) course policy
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
Then the settings are alphabetized
|
||||
|
||||
# Sauce labs does not play nicely with CodeMirror
|
||||
@skip_sauce
|
||||
Scenario: Test cancel editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key
|
||||
@@ -19,6 +22,8 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then the policy key value is unchanged
|
||||
|
||||
# Sauce labs does not play nicely with CodeMirror
|
||||
@skip_sauce
|
||||
Scenario: Test editing key value
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key and save
|
||||
@@ -26,6 +31,8 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then the policy key value is changed
|
||||
|
||||
# Sauce labs does not play nicely with CodeMirror
|
||||
@skip_sauce
|
||||
Scenario: Test how multi-line input appears
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object as a value for "discussion_topics"
|
||||
@@ -33,6 +40,8 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then it is displayed as formatted
|
||||
|
||||
# Sauce labs does not play nicely with CodeMirror
|
||||
@skip_sauce
|
||||
Scenario: Test error if value supplied is of the wrong type
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a JSON object as a value for "display_name"
|
||||
@@ -41,6 +50,8 @@ Feature: Advanced (manual) course policy
|
||||
Then the policy key value is unchanged
|
||||
|
||||
# This feature will work in Firefox only when Firefox is the active window
|
||||
# Sauce labs does not play nicely with CodeMirror
|
||||
@skip_sauce
|
||||
Scenario: Test automatic quoting of non-JSON values
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I create a non-JSON value not in quotes
|
||||
@@ -48,6 +59,8 @@ Feature: Advanced (manual) course policy
|
||||
And I reload the page
|
||||
Then it is displayed as a string
|
||||
|
||||
# Sauce labs does not play nicely with CodeMirror
|
||||
@skip_sauce
|
||||
Scenario: Confirmation is shown on save
|
||||
Given I am on the Advanced Course Settings page in Studio
|
||||
When I edit the value of a policy key
|
||||
|
||||
@@ -10,7 +10,10 @@ Feature: Course checklists
|
||||
Then I can check and uncheck tasks in a checklist
|
||||
And They are correctly selected after reloading the page
|
||||
|
||||
# CHROME ONLY, due to issues getting link to be active in firefox
|
||||
# There are issues getting link to be active in browsers other than chrome
|
||||
@skip_firefox
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: A task can link to a location within Studio
|
||||
Given I have opened Checklists
|
||||
When I select a link to the course outline
|
||||
@@ -18,7 +21,10 @@ Feature: Course checklists
|
||||
And I press the browser back button
|
||||
Then I am brought back to the course outline in the correct state
|
||||
|
||||
# CHROME ONLY, due to issues getting link to be active in firefox
|
||||
# There are issues getting link to be active in browsers other than chrome
|
||||
@skip_firefox
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: A task can link to a location outside Studio
|
||||
Given I have opened Checklists
|
||||
When I select a link to help page
|
||||
|
||||
@@ -64,6 +64,10 @@ Feature: Course Overview
|
||||
And I change an assignment's grading status
|
||||
Then I am shown a notification
|
||||
|
||||
# Notification is not shown on reorder for IE
|
||||
# Safari does not have moveMouseTo implemented
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: Notification is shown on subsection reorder
|
||||
Given I have opened a new course section in Studio
|
||||
And I have added a new subsection
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
Feature: Course Settings
|
||||
As a course author, I want to be able to configure my course settings.
|
||||
|
||||
# Safari has trouble keeps dates on refresh
|
||||
@skip_safari
|
||||
Scenario: User can set course dates
|
||||
Given I have opened a new course in Studio
|
||||
When I select Schedule and Details
|
||||
@@ -8,12 +10,16 @@ Feature: Course Settings
|
||||
And I press the "Save" notification button
|
||||
Then I see the set dates on refresh
|
||||
|
||||
# IE has trouble with saving information
|
||||
@skip_internetexplorer
|
||||
Scenario: User can clear previously set course dates (except start date)
|
||||
Given I have set course dates
|
||||
And I clear all the dates except start
|
||||
And I press the "Save" notification button
|
||||
Then I see cleared dates on refresh
|
||||
|
||||
# IE has trouble with saving information
|
||||
@skip_internetexplorer
|
||||
Scenario: User cannot clear the course start date
|
||||
Given I have set course dates
|
||||
And I press the "Save" notification button
|
||||
@@ -21,6 +27,10 @@ Feature: Course Settings
|
||||
Then I receive a warning about course start date
|
||||
And The previously set start date is shown on refresh
|
||||
|
||||
# IE has trouble with saving information
|
||||
# Safari gets CSRF token errors
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: User can correct the course start date warning
|
||||
Given I have tried to clear the course start
|
||||
And I have entered a new course start date
|
||||
@@ -28,12 +38,16 @@ Feature: Course Settings
|
||||
Then The warning about course start date goes away
|
||||
And My new course start date is shown on refresh
|
||||
|
||||
# Safari does not save + refresh properly through sauce labs
|
||||
@skip_safari
|
||||
Scenario: Settings are only persisted when saved
|
||||
Given I have set course dates
|
||||
And I press the "Save" notification button
|
||||
When I change fields
|
||||
Then I do not see the new changes persisted on refresh
|
||||
|
||||
# Safari does not save + refresh properly through sauce labs
|
||||
@skip_safari
|
||||
Scenario: Settings are reset on cancel
|
||||
Given I have set course dates
|
||||
And I press the "Save" notification button
|
||||
@@ -41,6 +55,8 @@ Feature: Course Settings
|
||||
And I press the "Cancel" notification button
|
||||
Then I do not see the changes
|
||||
|
||||
# Safari gets CSRF token errors
|
||||
@skip_safari
|
||||
Scenario: Confirmation is shown on save
|
||||
Given I have opened a new course in Studio
|
||||
When I select Schedule and Details
|
||||
|
||||
@@ -91,7 +91,7 @@ def remove_course_team_admin(_step, outer_capture, name):
|
||||
|
||||
@step(u'"([^"]*)" logs in$')
|
||||
def other_user_login(_step, name):
|
||||
world.browser.cookies.delete()
|
||||
world.visit('logout')
|
||||
world.visit('/')
|
||||
|
||||
signin_css = 'a.action-signin'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
Feature: Course updates
|
||||
As a course author, I want to be able to provide updates to my students
|
||||
|
||||
# Internet explorer can't select all so the update appears weirdly
|
||||
@skip_internetexplorer
|
||||
Scenario: Users can add updates
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
@@ -8,6 +10,8 @@ Feature: Course updates
|
||||
Then I should see the update "Hello"
|
||||
And I see a "saving" notification
|
||||
|
||||
# Internet explorer can't select all so the update appears weirdly
|
||||
@skip_internetexplorer
|
||||
Scenario: Users can edit updates
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
@@ -33,6 +37,8 @@ Feature: Course updates
|
||||
Then I should see the date "June 1, 2013"
|
||||
And I see a "saving" notification
|
||||
|
||||
# Internet explorer can't select all so the update appears weirdly
|
||||
@skip_internetexplorer
|
||||
Scenario: Users can change handouts
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the course updates page
|
||||
|
||||
@@ -6,6 +6,8 @@ Feature: Discussion Component Editor
|
||||
And I edit and select Settings
|
||||
Then I see three alphabetized settings and their expected values
|
||||
|
||||
# Safari doesn't save the name properly
|
||||
@skip_safari
|
||||
Scenario: User can modify display name
|
||||
Given I have created a Discussion Tag
|
||||
And I edit and select Settings
|
||||
|
||||
@@ -13,7 +13,7 @@ Feature: Course Grading
|
||||
When I add "6" new grades
|
||||
Then I see I now have "5" grades
|
||||
|
||||
#Cannot reliably make the delete button appear so using javascript instead
|
||||
# Cannot reliably make the delete button appear so using javascript instead
|
||||
Scenario: Users can delete grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
@@ -21,6 +21,9 @@ Feature: Course Grading
|
||||
And I delete a grade
|
||||
Then I see I now have "2" grades
|
||||
|
||||
# IE and Safari cannot reliably drag and drop through selenium
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: Users can move grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
@@ -85,6 +88,9 @@ Feature: Course Grading
|
||||
When I change assignment type "Homework" to ""
|
||||
Then the save button is disabled
|
||||
|
||||
# IE and Safari cannot type in grade range name
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: User can edit grading range names
|
||||
Given I have opened a new course in Studio
|
||||
And I have populated the course
|
||||
|
||||
@@ -112,10 +112,10 @@ def changes_not_persisted(step):
|
||||
|
||||
@step(u'I see the assignment type "(.*)"$')
|
||||
def i_see_the_assignment_type(_step, name):
|
||||
assignment_css = '#course-grading-assignment-name'
|
||||
assignments = world.css_find(assignment_css)
|
||||
types = [ele['value'] for ele in assignments]
|
||||
assert name in types
|
||||
assignment_css = '#course-grading-assignment-name'
|
||||
assignments = world.css_find(assignment_css)
|
||||
types = [ele['value'] for ele in assignments]
|
||||
assert name in types
|
||||
|
||||
|
||||
@step(u'I change the highest grade range to "(.*)"$')
|
||||
@@ -144,6 +144,7 @@ def cannot_edit_fail(_step):
|
||||
pass # We should get this exception on failing to edit the element
|
||||
|
||||
|
||||
|
||||
@step(u'I change the grace period to "(.*)"$')
|
||||
def i_change_grace_period(_step, grace_period):
|
||||
grace_period_css = '#course-grading-graceperiod'
|
||||
|
||||
@@ -6,6 +6,8 @@ Feature: HTML Editor
|
||||
And I edit and select Settings
|
||||
Then I see only the HTML display name setting
|
||||
|
||||
# Safari doesn't save the name properly
|
||||
@skip_safari
|
||||
Scenario: User can modify display name
|
||||
Given I have created a Blank HTML Page
|
||||
And I edit and select Settings
|
||||
|
||||
@@ -7,12 +7,16 @@ Feature: Problem Editor
|
||||
Then I see five alphabetized settings and their expected values
|
||||
And Edit High Level Source is not visible
|
||||
|
||||
# Safari is having trouble saving the values on sauce
|
||||
@skip_safari
|
||||
Scenario: User can modify String values
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
|
||||
# Safari is having trouble saving the values on sauce
|
||||
@skip_safari
|
||||
Scenario: User can specify special characters in String values
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
@@ -25,6 +29,8 @@ Feature: Problem Editor
|
||||
Then I can revert the display name to unset
|
||||
And my display name is unset on save
|
||||
|
||||
# IE will not click the revert button properly
|
||||
@skip_internetexplorer
|
||||
Scenario: User can select values in a Select
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
@@ -32,6 +38,8 @@ Feature: Problem Editor
|
||||
And my change to randomization is persisted
|
||||
And I can revert to the default value for randomization
|
||||
|
||||
# Safari will input it as 35.
|
||||
@skip_safari
|
||||
Scenario: User can modify float input values
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
@@ -44,16 +52,22 @@ Feature: Problem Editor
|
||||
When I edit and select Settings
|
||||
Then if I set the weight to "abc", it remains unset
|
||||
|
||||
# Safari will input it as 234.
|
||||
@skip_safari
|
||||
Scenario: User cannot type decimal values integer number field
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then if I set the max attempts to "2.34", it will persist as a valid integer
|
||||
|
||||
# Safari will input it incorrectly
|
||||
@skip_safari
|
||||
Scenario: User cannot type out of range values in an integer number field
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
Then if I set the max attempts to "-3", it will persist as a valid integer
|
||||
|
||||
# Safari will input it as 35.
|
||||
@skip_safari
|
||||
Scenario: Settings changes are not saved on Cancel
|
||||
Given I have created a Blank Common Problem
|
||||
When I edit and select Settings
|
||||
@@ -67,6 +81,8 @@ Feature: Problem Editor
|
||||
Then Edit High Level Source is visible
|
||||
|
||||
# This feature will work in Firefox only when Firefox is the active window
|
||||
# IE will not interact with the high level source in sauce labs
|
||||
@skip_internetexplorer
|
||||
Scenario: High Level source is persisted for LaTeX problem (bug STUD-280)
|
||||
Given I have created a LaTeX Problem
|
||||
When I edit and compile the High Level Source
|
||||
|
||||
@@ -15,6 +15,8 @@ Feature: Static Pages
|
||||
And I "delete" the "Empty" page
|
||||
Then I should not see a "Empty" static page
|
||||
|
||||
# Safari won't update the name properly
|
||||
@skip_safari
|
||||
Scenario: Users can edit static pages
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the static pages page
|
||||
|
||||
@@ -25,6 +25,8 @@ Feature: Create Subsection
|
||||
And I reload the page
|
||||
Then I see it marked as Homework
|
||||
|
||||
# Safari has trouble saving the date in Sauce
|
||||
@skip_safari
|
||||
Scenario: Set a due date in a different year (bug #256)
|
||||
Given I have opened a new subsection in Studio
|
||||
And I set the subsection release date to 12/25/2011 03:00
|
||||
|
||||
@@ -5,6 +5,9 @@ Feature: Textbooks
|
||||
When I go to the textbooks page
|
||||
Then I should see a message telling me to create a new textbook
|
||||
|
||||
# IE and Safari on sauce labs will not upload the textbook correctly resulting in an error
|
||||
@skip_internetexplorer
|
||||
@skip_safari
|
||||
Scenario: Create a textbook
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the textbooks page
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
Feature: Upload Files
|
||||
As a course author, I want to be able to upload files for my students
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Users can upload files
|
||||
Given I have opened a new course in Studio
|
||||
And I go to the files and uploads page
|
||||
@@ -8,6 +10,8 @@ Feature: Upload Files
|
||||
Then I should see the file "test" was uploaded
|
||||
And The url for the file "test" is valid
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Users can update files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
@@ -15,6 +19,8 @@ Feature: Upload Files
|
||||
And I upload the file "test"
|
||||
Then I should see only one "test"
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Users can delete uploaded files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
@@ -23,12 +29,16 @@ Feature: Upload Files
|
||||
Then I should not see the file "test" was uploaded
|
||||
And I see a confirmation that the file was deleted
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Users can download files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
When I upload the file "test"
|
||||
Then I can download the correct "test" file
|
||||
|
||||
# Uploading isn't working on safari with sauce labs
|
||||
@skip_safari
|
||||
Scenario: Users can download updated files
|
||||
Given I have opened a new course in studio
|
||||
And I go to the files and uploads page
|
||||
|
||||
@@ -10,6 +10,7 @@ import os
|
||||
|
||||
TEST_ROOT = settings.COMMON_TEST_DATA_ROOT
|
||||
|
||||
|
||||
@step(u'I go to the files and uploads page')
|
||||
def go_to_uploads(_step):
|
||||
menu_css = 'li.nav-course-courseware'
|
||||
@@ -106,8 +107,8 @@ def get_index(file_name):
|
||||
def get_file(file_name):
|
||||
index = get_index(file_name)
|
||||
assert index != -1
|
||||
|
||||
url_css = 'a.filename'
|
||||
|
||||
def get_url():
|
||||
return world.css_find(url_css)[index]._element.get_attribute('href')
|
||||
url = world.retry_on_exception(get_url)
|
||||
|
||||
@@ -6,17 +6,23 @@ Feature: Video Component Editor
|
||||
And I edit the component
|
||||
Then I see the correct video settings and default values
|
||||
|
||||
# Safari has trouble saving values on Sauce
|
||||
@skip_safari
|
||||
Scenario: User can modify Video display name
|
||||
Given I have created a Video component
|
||||
And I edit the component
|
||||
Then I can modify the display name
|
||||
And my video display name change is persisted on save
|
||||
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are hidden when "show captions" is false
|
||||
Given I have created a Video component
|
||||
And I have set "show captions" to False
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are shown when "show captions" is true
|
||||
Given I have created a Video component
|
||||
And I have set "show captions" to True
|
||||
|
||||
@@ -10,15 +10,21 @@ Feature: Video Component
|
||||
Given I have clicked the new unit button
|
||||
Then creating a video takes a single click
|
||||
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are hidden correctly
|
||||
Given I have created a Video component
|
||||
And I have hidden captions
|
||||
Then when I view the video it does not show the captions
|
||||
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are shown correctly
|
||||
Given I have created a Video component
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
# Sauce Labs cannot delete cookies
|
||||
@skip_sauce
|
||||
Scenario: Captions are toggled correctly
|
||||
Given I have created a Video component
|
||||
And I have toggled captions
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Script for importing courseware from XML format
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.base import BaseCommand, CommandError, make_option
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
@@ -14,18 +14,26 @@ class Command(BaseCommand):
|
||||
"""
|
||||
help = 'Import the specified data directory into the default ModuleStore'
|
||||
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--nostatic',
|
||||
action='store_true',
|
||||
help='Skip import of static content'),
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"Execute the command"
|
||||
if len(args) == 0:
|
||||
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
|
||||
raise CommandError("import requires at least one argument: <data directory> [--nostatic] [<course dir>...]")
|
||||
|
||||
data_dir = args[0]
|
||||
do_import_static = not (options.get('nostatic', False))
|
||||
if len(args) > 1:
|
||||
course_dirs = args[1:]
|
||||
else:
|
||||
course_dirs = None
|
||||
print("Importing. Data_dir={data}, course_dirs={courses}".format(
|
||||
data=data_dir,
|
||||
courses=course_dirs))
|
||||
courses=course_dirs,
|
||||
dis=do_import_static))
|
||||
import_from_xml(modulestore('direct'), data_dir, course_dirs, load_error_modules=False,
|
||||
static_content_store=contentstore(), verbose=True)
|
||||
static_content_store=contentstore(), verbose=True, do_import_static=do_import_static)
|
||||
|
||||
@@ -60,11 +60,11 @@ class UploadTestCase(CourseTestCase):
|
||||
f = BytesIO("sample content")
|
||||
f.name = "sample.txt"
|
||||
resp = self.client.post(self.url, {"name": "my-name", "file": f})
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
def test_no_file(self):
|
||||
resp = self.client.post(self.url, {"name": "file.txt"})
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
def test_get(self):
|
||||
resp = self.client.get(self.url)
|
||||
|
||||
@@ -486,7 +486,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
content_store = contentstore()
|
||||
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store)
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, verbose=True)
|
||||
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
course = module_store.get_item(course_location)
|
||||
@@ -962,8 +962,17 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
'vertical', 'vertical_test', None]), depth=1)
|
||||
|
||||
self.assertTrue(getattr(vertical, 'is_draft', False))
|
||||
self.assertNotIn('index_in_children_list', child.xml_attributes)
|
||||
self.assertNotIn('parent_sequential_url', vertical.xml_attributes)
|
||||
|
||||
for child in vertical.get_children():
|
||||
self.assertTrue(getattr(child, 'is_draft', False))
|
||||
self.assertNotIn('index_in_children_list', child.xml_attributes)
|
||||
if hasattr(child, 'data'):
|
||||
self.assertNotIn('index_in_children_list', child.data)
|
||||
self.assertNotIn('parent_sequential_url', child.xml_attributes)
|
||||
if hasattr(child, 'data'):
|
||||
self.assertNotIn('parent_sequential_url', child.data)
|
||||
|
||||
# make sure that we don't have a sequential that is in draft mode
|
||||
sequential = draft_store.get_item(Location(['i4x', 'edX', 'toy',
|
||||
@@ -1074,6 +1083,38 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
# It should now contain empty data
|
||||
self.assertEquals(imported_word_cloud.data, '')
|
||||
|
||||
def test_html_export_roundtrip(self):
|
||||
"""
|
||||
Test that a course which has HTML that has style formatting is preserved in export/import
|
||||
"""
|
||||
module_store = modulestore('direct')
|
||||
content_store = contentstore()
|
||||
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'])
|
||||
|
||||
location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
|
||||
# Export the course
|
||||
root_dir = path(mkdtemp_clean())
|
||||
export_to_xml(module_store, content_store, location, root_dir, 'test_roundtrip')
|
||||
|
||||
# Reimport and get the video back
|
||||
import_from_xml(module_store, root_dir)
|
||||
|
||||
# get the sample HTML with styling information
|
||||
html_module = module_store.get_instance(
|
||||
'edX/toy/2012_Fall',
|
||||
Location(['i4x', 'edX', 'toy', 'html', 'with_styling'])
|
||||
)
|
||||
self.assertIn('<p style="font:italic bold 72px/30px Georgia, serif; color: red; ">', html_module.data)
|
||||
|
||||
# get the sample HTML with just a simple <img> tag information
|
||||
html_module = module_store.get_instance(
|
||||
'edX/toy/2012_Fall',
|
||||
Location(['i4x', 'edX', 'toy', 'html', 'just_img'])
|
||||
)
|
||||
self.assertIn('<img src="/static/foo_bar.jpg" />', html_module.data)
|
||||
|
||||
def test_course_handouts_rewrites(self):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
@@ -1384,7 +1425,7 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Chapter 2')
|
||||
|
||||
# go to various pages
|
||||
@@ -1394,92 +1435,92 @@ class ContentStoreTest(ModuleStoreTestCase):
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# export page
|
||||
resp = self.client.get(reverse('export_course',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# manage users
|
||||
resp = self.client.get(reverse('manage_users',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# course info
|
||||
resp = self.client.get(reverse('course_info',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# settings_details
|
||||
resp = self.client.get(reverse('settings_details',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# settings_details
|
||||
resp = self.client.get(reverse('settings_grading',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# static_pages
|
||||
resp = self.client.get(reverse('static_pages',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'coursename': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# static_pages
|
||||
resp = self.client.get(reverse('asset_index',
|
||||
kwargs={'org': loc.org,
|
||||
'course': loc.course,
|
||||
'name': loc.name}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# go look at a subsection page
|
||||
subsection_location = loc.replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.get(reverse('edit_subsection',
|
||||
kwargs={'location': subsection_location.url()}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# go look at the Edit page
|
||||
unit_location = loc.replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.get(reverse('edit_unit',
|
||||
kwargs={'location': unit_location.url()}))
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# delete a component
|
||||
del_loc = loc.replace(category='html', name='test_html')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc.replace(category='vertical', name='test_vertical')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
# delete a unit
|
||||
del_loc = loc.replace(category='sequential', name='test_sequence')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
# delete a chapter
|
||||
del_loc = loc.replace(category='chapter', name='chapter_2')
|
||||
resp = self.client.post(reverse('delete_item'),
|
||||
json.dumps({'id': del_loc.url()}), "application/json")
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
def test_import_into_new_course_id(self):
|
||||
module_store = modulestore('direct')
|
||||
|
||||
@@ -439,12 +439,12 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
|
||||
def test_get(self):
|
||||
resp = self.client.get(self.url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
|
||||
def test_delete(self):
|
||||
resp = self.client.delete(self.url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
def test_post(self):
|
||||
grader = {
|
||||
@@ -455,5 +455,5 @@ class CourseGraderUpdatesTest(CourseTestCase):
|
||||
"weight": 17.3,
|
||||
}
|
||||
resp = self.client.post(self.url, grader)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
|
||||
128
cms/djangoapps/contentstore/tests/test_import_nostatic.py
Normal file
128
cms/djangoapps/contentstore/tests/test_import_nostatic.py
Normal file
@@ -0,0 +1,128 @@
|
||||
#pylint: disable=E1101
|
||||
'''
|
||||
Tests for importing with no static
|
||||
'''
|
||||
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from path import path
|
||||
import copy
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.contentstore.django import _CONTENTSTORE
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from uuid import uuid4
|
||||
from pymongo import MongoClient
|
||||
|
||||
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
|
||||
TEST_DATA_CONTENTSTORE['OPTIONS']['db'] = 'test_xcontent_%s' % uuid4().hex
|
||||
|
||||
|
||||
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
|
||||
class ContentStoreImportNoStaticTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests that rely on the toy and test_import_course courses.
|
||||
NOTE: refactor using CourseFactory so they do not.
|
||||
"""
|
||||
def setUp(self):
|
||||
|
||||
settings.MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
settings.MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
|
||||
uname = 'testuser'
|
||||
email = 'test+courses@edx.org'
|
||||
password = 'foo'
|
||||
|
||||
# Create the use so we can log them in.
|
||||
self.user = User.objects.create_user(uname, email, password)
|
||||
|
||||
# Note that we do not actually need to do anything
|
||||
# for registration if we directly mark them active.
|
||||
self.user.is_active = True
|
||||
# Staff has access to view all courses
|
||||
self.user.is_staff = True
|
||||
|
||||
# Save the data that we've just changed to the db.
|
||||
self.user.save()
|
||||
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
def tearDown(self):
|
||||
MongoClient().drop_database(TEST_DATA_CONTENTSTORE['OPTIONS']['db'])
|
||||
_CONTENTSTORE.clear()
|
||||
|
||||
def load_test_import_course(self):
|
||||
'''
|
||||
Load the standard course used to test imports (for do_import_static=False behavior).
|
||||
'''
|
||||
content_store = contentstore()
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['test_import_course'], static_content_store=content_store, do_import_static=False, verbose=True)
|
||||
course_location = CourseDescriptor.id_to_location('edX/test_import_course/2012_Fall')
|
||||
course = module_store.get_item(course_location)
|
||||
self.assertIsNotNone(course)
|
||||
|
||||
return module_store, content_store, course, course_location
|
||||
|
||||
def test_static_import(self):
|
||||
'''
|
||||
Stuff in static_import should always be imported into contentstore
|
||||
'''
|
||||
_, content_store, course, course_location = self.load_test_import_course()
|
||||
|
||||
# make sure we have ONE asset in our contentstore ("should_be_imported.html")
|
||||
all_assets = content_store.get_all_content_for_course(course_location)
|
||||
print "len(all_assets)=%d" % len(all_assets)
|
||||
self.assertEqual(len(all_assets), 1)
|
||||
|
||||
content = None
|
||||
try:
|
||||
location = StaticContent.get_location_from_path('/c4x/edX/test_import_course/asset/should_be_imported.html')
|
||||
content = content_store.find(location)
|
||||
except NotFoundError:
|
||||
pass
|
||||
|
||||
self.assertIsNotNone(content)
|
||||
|
||||
# make sure course.lms.static_asset_path is correct
|
||||
print "static_asset_path = {0}".format(course.lms.static_asset_path)
|
||||
self.assertEqual(course.lms.static_asset_path, 'test_import_course')
|
||||
|
||||
def test_asset_import_nostatic(self):
|
||||
'''
|
||||
This test validates that an image asset is NOT imported when do_import_static=False
|
||||
'''
|
||||
content_store = contentstore()
|
||||
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], static_content_store=content_store, do_import_static=False, verbose=True)
|
||||
|
||||
course_location = CourseDescriptor.id_to_location('edX/toy/2012_Fall')
|
||||
module_store.get_item(course_location)
|
||||
|
||||
# make sure we have NO assets in our contentstore
|
||||
all_assets = content_store.get_all_content_for_course(course_location)
|
||||
print "len(all_assets)=%d" % len(all_assets)
|
||||
self.assertEqual(len(all_assets), 0)
|
||||
|
||||
def test_no_static_link_rewrites_on_import(self):
|
||||
module_store = modulestore('direct')
|
||||
import_from_xml(module_store, 'common/test/data/', ['toy'], do_import_static=False, verbose=True)
|
||||
|
||||
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'course_info', 'handouts', None]))
|
||||
self.assertIn('/static/', handouts.data)
|
||||
|
||||
handouts = module_store.get_item(Location(['i4x', 'edX', 'toy', 'html', 'toyhtml', None]))
|
||||
self.assertIn('/static/', handouts.data)
|
||||
@@ -34,7 +34,7 @@ class DeleteItem(CourseTestCase):
|
||||
resp.content,
|
||||
"application/json"
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
|
||||
class TestCreateItem(CourseTestCase):
|
||||
|
||||
@@ -23,7 +23,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
def test_view_index(self):
|
||||
"Basic check that the textbook index page responds correctly"
|
||||
resp = self.client.get(self.url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# we don't have resp.context right now,
|
||||
# due to bugs in our testing harness :(
|
||||
if resp.context:
|
||||
@@ -36,7 +36,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
self.assertEqual(self.course.pdf_textbooks, obj)
|
||||
|
||||
@@ -73,7 +73,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
self.assertEqual(content, obj)
|
||||
|
||||
@@ -90,7 +90,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# reload course
|
||||
store = get_modulestore(self.course.location)
|
||||
@@ -111,7 +111,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
obj = json.loads(resp.content)
|
||||
self.assertIn("error", obj)
|
||||
|
||||
@@ -184,7 +184,7 @@ class TextbookCreateTestCase(CourseTestCase):
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertNotIn("Location", resp)
|
||||
|
||||
|
||||
@@ -238,14 +238,14 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
def test_get_1(self):
|
||||
"Get the first textbook"
|
||||
resp = self.client.get(self.url1)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
compare = json.loads(resp.content)
|
||||
self.assertEqual(compare, self.textbook1)
|
||||
|
||||
def test_get_2(self):
|
||||
"Get the second textbook"
|
||||
resp = self.client.get(self.url2)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
compare = json.loads(resp.content)
|
||||
self.assertEqual(compare, self.textbook2)
|
||||
|
||||
@@ -257,7 +257,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
def test_delete(self):
|
||||
"Delete a textbook by ID"
|
||||
resp = self.client.delete(self.url1)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
course = self.store.get_item(self.course.location)
|
||||
self.assertEqual(course.pdf_textbooks, [self.textbook2])
|
||||
|
||||
@@ -288,7 +288,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
)
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
resp2 = self.client.get(url)
|
||||
self.assert2XX(resp2.status_code)
|
||||
self.assertEqual(resp2.status_code, 200)
|
||||
compare = json.loads(resp2.content)
|
||||
self.assertEqual(compare, textbook)
|
||||
course = self.store.get_item(self.course.location)
|
||||
@@ -311,7 +311,7 @@ class TextbookByIdTestCase(CourseTestCase):
|
||||
)
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
resp2 = self.client.get(self.url2)
|
||||
self.assert2XX(resp2.status_code)
|
||||
self.assertEqual(resp2.status_code, 200)
|
||||
compare = json.loads(resp2.content)
|
||||
self.assertEqual(compare, replacement)
|
||||
course = self.store.get_item(self.course.location)
|
||||
|
||||
@@ -72,13 +72,13 @@ class UsersTestCase(CourseTestCase):
|
||||
|
||||
def test_detail_inactive(self):
|
||||
resp = self.client.get(self.inactive_detail_url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
result = json.loads(resp.content)
|
||||
self.assertFalse(result["active"])
|
||||
|
||||
def test_detail_invalid(self):
|
||||
resp = self.client.get(self.invalid_detail_url)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
|
||||
@@ -87,7 +87,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
data={"role": None},
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -103,7 +103,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -122,7 +122,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -142,7 +142,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -157,7 +157,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
self.assert_not_enrolled()
|
||||
@@ -169,7 +169,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
self.assert_not_enrolled()
|
||||
@@ -180,7 +180,7 @@ class UsersTestCase(CourseTestCase):
|
||||
data={"role": "staff"},
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -197,7 +197,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -214,7 +214,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
ext_user = User.objects.get(email=self.ext_user.email)
|
||||
groups = [g.name for g in ext_user.groups.all()]
|
||||
@@ -273,7 +273,7 @@ class UsersTestCase(CourseTestCase):
|
||||
data={"role": "instructor"},
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
|
||||
@@ -288,7 +288,7 @@ class UsersTestCase(CourseTestCase):
|
||||
data={"role": "instructor"},
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
|
||||
@@ -306,7 +306,7 @@ class UsersTestCase(CourseTestCase):
|
||||
})
|
||||
|
||||
resp = self.client.delete(self_url)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
# reload user from DB
|
||||
user = User.objects.get(email=self.user.email)
|
||||
groups = [g.name for g in user.groups.all()]
|
||||
@@ -321,7 +321,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.ext_user.save()
|
||||
|
||||
resp = self.client.delete(self.detail_url)
|
||||
self.assert4XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
result = json.loads(resp.content)
|
||||
self.assertIn("error", result)
|
||||
# reload user from DB
|
||||
@@ -347,7 +347,7 @@ class UsersTestCase(CourseTestCase):
|
||||
self.detail_url,
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
self.assert_enrolled()
|
||||
|
||||
def test_staff_to_instructor_still_enrolled(self):
|
||||
@@ -366,7 +366,7 @@ class UsersTestCase(CourseTestCase):
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
)
|
||||
self.assert2XX(resp.status_code)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
self.assert_enrolled()
|
||||
|
||||
def assert_not_enrolled(self):
|
||||
|
||||
@@ -8,6 +8,7 @@ so that we can run the lettuce acceptance tests.
|
||||
# pylint: disable=W0401, W0614
|
||||
|
||||
from .test import *
|
||||
from lms.envs.sauce import *
|
||||
|
||||
# You need to start the server in debug mode,
|
||||
# otherwise the browser will not render the pages correctly
|
||||
@@ -17,7 +18,7 @@ DEBUG = True
|
||||
import logging
|
||||
logging.disable(logging.ERROR)
|
||||
import os
|
||||
import random
|
||||
from random import choice, randint
|
||||
|
||||
|
||||
def seed():
|
||||
@@ -75,7 +76,6 @@ DATABASES = {
|
||||
# Use the auto_auth workflow for creating users and logging them in
|
||||
MITX_FEATURES['AUTOMATIC_AUTH_FOR_TESTING'] = True
|
||||
|
||||
|
||||
# HACK
|
||||
# Setting this flag to false causes imports to not load correctly in the lettuce python files
|
||||
# We do not yet understand why this occurs. Setting this to true is a stopgap measure
|
||||
@@ -84,5 +84,5 @@ USE_I18N = True
|
||||
# Include the lettuce app for acceptance testing, including the 'harvest' django-admin command
|
||||
INSTALLED_APPS += ('lettuce.django',)
|
||||
LETTUCE_APPS = ('contentstore',)
|
||||
LETTUCE_SERVER_PORT = random.randint(1024, 65535)
|
||||
LETTUCE_BROWSER = 'chrome'
|
||||
LETTUCE_SERVER_PORT = choice(PORTS) if SAUCE.get('SAUCE_ENABLED') else randint(1024, 65535)
|
||||
LETTUCE_BROWSER = os.environ.get('LETTUCE_BROWSER', 'chrome')
|
||||
|
||||
@@ -8,19 +8,20 @@ from course_groups.models import CourseUserGroup
|
||||
from course_groups.cohorts import (get_cohort, get_course_cohorts,
|
||||
is_commentable_cohorted, get_cohort_by_name)
|
||||
|
||||
from xmodule.modulestore.django import modulestore, _MODULESTORES
|
||||
from xmodule.modulestore.django import modulestore, clear_existing_modulestores
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import xml_store_config
|
||||
from xmodule.modulestore.tests.django_utils import mixed_store_config
|
||||
|
||||
# NOTE: running this with the lms.envs.test config works without
|
||||
# manually overriding the modulestore. However, running with
|
||||
# cms.envs.test doesn't.
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
TEST_MAPPING = {'edX/toy/2012_Fall': 'xml'}
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, TEST_MAPPING)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class TestCohorts(django.test.TestCase):
|
||||
|
||||
@staticmethod
|
||||
@@ -82,9 +83,7 @@ class TestCohorts(django.test.TestCase):
|
||||
"""
|
||||
Make sure that course is reloaded every time--clear out the modulestore.
|
||||
"""
|
||||
# don't like this, but don't know a better way to undo all changes made
|
||||
# to course. We don't have a course.clone() method.
|
||||
_MODULESTORES.clear()
|
||||
clear_existing_modulestores()
|
||||
|
||||
def test_get_cohort(self):
|
||||
"""
|
||||
|
||||
@@ -33,6 +33,7 @@ class CourseMode(models.Model):
|
||||
currency = models.CharField(default="usd", max_length=8)
|
||||
|
||||
DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd')
|
||||
DEFAULT_MODE_SLUG = 'honor'
|
||||
|
||||
class Meta:
|
||||
""" meta attributes of this model """
|
||||
@@ -51,3 +52,18 @@ class CourseMode(models.Model):
|
||||
if not modes:
|
||||
modes = [cls.DEFAULT_MODE]
|
||||
return modes
|
||||
|
||||
@classmethod
|
||||
def mode_for_course(cls, course_id, mode_slug):
|
||||
"""
|
||||
Returns the mode for the course corresponding to mode_slug.
|
||||
|
||||
If this particular mode is not set for the course, returns None
|
||||
"""
|
||||
modes = cls.modes_for_course(course_id)
|
||||
|
||||
matched = [m for m in modes if m.slug == mode_slug]
|
||||
if matched:
|
||||
return matched[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -60,3 +60,6 @@ class CourseModeModelTest(TestCase):
|
||||
|
||||
modes = CourseMode.modes_for_course(self.course_id)
|
||||
self.assertEqual(modes, set_modes)
|
||||
self.assertEqual(mode1, CourseMode.mode_for_course(self.course_id, u'honor'))
|
||||
self.assertEqual(mode2, CourseMode.mode_for_course(self.course_id, u'verified'))
|
||||
self.assertIsNone(CourseMode.mode_for_course(self.course_id, 'DNE'))
|
||||
|
||||
@@ -14,11 +14,9 @@ from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.modulestore.django import editable_modulestore
|
||||
|
||||
from external_auth.models import ExternalAuthMap
|
||||
from external_auth.views import shib_login, course_specific_login, course_specific_register
|
||||
@@ -27,6 +25,8 @@ from student.views import create_account, change_enrollment
|
||||
from student.models import UserProfile, Registration, CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
|
||||
|
||||
# Shib is supposed to provide 'REMOTE_USER', 'givenName', 'sn', 'mail', 'Shib-Identity-Provider'
|
||||
# attributes via request.META. We can count on 'Shib-Identity-Provider', and 'REMOTE_USER' being present
|
||||
# b/c of how mod_shib works but should test the behavior with the rest of the attributes present/missing
|
||||
@@ -64,7 +64,7 @@ def gen_all_identities():
|
||||
yield _build_identity_dict(mail, given_name, surname)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE, SESSION_ENGINE='django.contrib.sessions.backends.cache')
|
||||
class ShibSPTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the Shibboleth SP, which communicates via request.META
|
||||
@@ -73,7 +73,7 @@ class ShibSPTest(ModuleStoreTestCase):
|
||||
request_factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
self.store = modulestore()
|
||||
self.store = editable_modulestore()
|
||||
|
||||
@unittest.skipUnless(settings.MITX_FEATURES.get('AUTH_USE_SHIB'), True)
|
||||
def test_exception_shib_login(self):
|
||||
|
||||
@@ -90,7 +90,7 @@ def replace_course_urls(text, course_id):
|
||||
return re.sub(_url_replace_regex('/course/'), replace_course_url, text)
|
||||
|
||||
|
||||
def replace_static_urls(text, data_directory, course_id=None):
|
||||
def replace_static_urls(text, data_directory, course_id=None, static_asset_path=''):
|
||||
"""
|
||||
Replace /static/$stuff urls either with their correct url as generated by collectstatic,
|
||||
(/static/$md5_hashed_stuff) or by the course-specific content static url
|
||||
@@ -100,6 +100,7 @@ def replace_static_urls(text, data_directory, course_id=None):
|
||||
text: The source text to do the substitution in
|
||||
data_directory: The directory in which course data is stored
|
||||
course_id: The course identifier used to distinguish static content for this course in studio
|
||||
static_asset_path: Path for static assets, which overrides data_directory and course_namespace, if nonempty
|
||||
"""
|
||||
|
||||
def replace_static_url(match):
|
||||
@@ -116,7 +117,7 @@ def replace_static_urls(text, data_directory, course_id=None):
|
||||
if settings.DEBUG and finders.find(rest, True):
|
||||
return original
|
||||
# if we're running with a MongoBacked store course_namespace is not None, then use studio style urls
|
||||
elif course_id and modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE:
|
||||
elif (not static_asset_path) and course_id and modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE:
|
||||
# first look in the static file pipeline and see if we are trying to reference
|
||||
# a piece of static content which is in the mitx repo (e.g. JS associated with an xmodule)
|
||||
|
||||
@@ -135,7 +136,7 @@ def replace_static_urls(text, data_directory, course_id=None):
|
||||
url = StaticContent.convert_legacy_static_url_with_course_id(rest, course_id)
|
||||
# Otherwise, look the file up in staticfiles_storage, and append the data directory if needed
|
||||
else:
|
||||
course_path = "/".join((data_directory, rest))
|
||||
course_path = "/".join((static_asset_path or data_directory, rest))
|
||||
|
||||
try:
|
||||
if staticfiles_storage.exists(rest):
|
||||
@@ -152,7 +153,7 @@ def replace_static_urls(text, data_directory, course_id=None):
|
||||
|
||||
|
||||
return re.sub(
|
||||
_url_replace_regex('/static/(?!{data_dir})'.format(data_dir=data_directory)),
|
||||
_url_replace_regex('/static/(?!{data_dir})'.format(data_dir=static_asset_path or data_directory)),
|
||||
replace_static_url,
|
||||
text
|
||||
)
|
||||
|
||||
@@ -827,9 +827,6 @@ class CourseEnrollment(models.Model):
|
||||
@classmethod
|
||||
def is_enrolled(cls, user, course_id):
|
||||
"""
|
||||
Remove the user from a given course. If the relevant `CourseEnrollment`
|
||||
object doesn't exist, we log an error but don't throw an exception.
|
||||
|
||||
Returns True if the user is enrolled in the course (the entry must exist
|
||||
and it must have `is_active=True`). Otherwise, returns False.
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ from logging import getLogger
|
||||
from django.core.management import call_command
|
||||
from django.conf import settings
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
from requests import put
|
||||
from base64 import encodestring
|
||||
from json import dumps
|
||||
|
||||
# Let the LMS and CMS do their one-time setup
|
||||
# For example, setting up mongo caches
|
||||
@@ -42,43 +46,93 @@ LOGGER.info("Loading the lettuce acceptance testing terrain file...")
|
||||
MAX_VALID_BROWSER_ATTEMPTS = 20
|
||||
|
||||
|
||||
def get_username_and_key():
|
||||
"""
|
||||
Returns the Sauce Labs username and access ID as set by environment variables
|
||||
"""
|
||||
return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')}
|
||||
|
||||
|
||||
def set_job_status(jobid, passed=True):
|
||||
"""
|
||||
Sets the job status on sauce labs
|
||||
"""
|
||||
body_content = dumps({"passed": passed})
|
||||
config = get_username_and_key()
|
||||
base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1]
|
||||
result = put('http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid),
|
||||
data=body_content,
|
||||
headers={"Authorization": "Basic {}".format(base64string)})
|
||||
return result.status_code == 200
|
||||
|
||||
|
||||
def make_desired_capabilities():
|
||||
"""
|
||||
Returns a DesiredCapabilities object corresponding to the environment sauce parameters
|
||||
"""
|
||||
desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME)
|
||||
desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM')
|
||||
desired_capabilities['version'] = settings.SAUCE.get('VERSION')
|
||||
desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE')
|
||||
desired_capabilities['name'] = settings.SAUCE.get('SESSION')
|
||||
desired_capabilities['build'] = settings.SAUCE.get('BUILD')
|
||||
desired_capabilities['video-upload-on-pass'] = False
|
||||
desired_capabilities['sauce-advisor'] = False
|
||||
desired_capabilities['record-screenshots'] = False
|
||||
desired_capabilities['selenium-version'] = "2.34.0"
|
||||
desired_capabilities['max-duration'] = 3600
|
||||
desired_capabilities['public'] = 'public restricted'
|
||||
return desired_capabilities
|
||||
|
||||
|
||||
@before.harvest
|
||||
def initial_setup(server):
|
||||
"""
|
||||
Launch the browser once before executing the tests.
|
||||
"""
|
||||
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
|
||||
world.absorb(settings.SAUCE.get('SAUCE_ENABLED'), 'SAUCE_ENABLED')
|
||||
|
||||
# There is an issue with ChromeDriver2 r195627 on Ubuntu
|
||||
# in which we sometimes get an invalid browser session.
|
||||
# This is a work-around to ensure that we get a valid session.
|
||||
success = False
|
||||
num_attempts = 0
|
||||
while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
|
||||
if not world.SAUCE_ENABLED:
|
||||
browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')
|
||||
|
||||
# Get a browser session
|
||||
world.browser = Browser(browser_driver)
|
||||
# There is an issue with ChromeDriver2 r195627 on Ubuntu
|
||||
# in which we sometimes get an invalid browser session.
|
||||
# This is a work-around to ensure that we get a valid session.
|
||||
success = False
|
||||
num_attempts = 0
|
||||
while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
|
||||
world.browser = Browser(browser_driver)
|
||||
|
||||
# Try to visit the main page
|
||||
# If the browser session is invalid, this will
|
||||
# raise a WebDriverException
|
||||
try:
|
||||
world.visit('/')
|
||||
# Try to visit the main page
|
||||
# If the browser session is invalid, this will
|
||||
# raise a WebDriverException
|
||||
try:
|
||||
world.visit('/')
|
||||
|
||||
except WebDriverException:
|
||||
world.browser.quit()
|
||||
num_attempts += 1
|
||||
except WebDriverException:
|
||||
world.browser.quit()
|
||||
num_attempts += 1
|
||||
|
||||
else:
|
||||
success = True
|
||||
else:
|
||||
success = True
|
||||
|
||||
# If we were unable to get a valid session within the limit of attempts,
|
||||
# then we cannot run the tests.
|
||||
if not success:
|
||||
raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
|
||||
# If we were unable to get a valid session within the limit of attempts,
|
||||
# then we cannot run the tests.
|
||||
if not success:
|
||||
raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
|
||||
|
||||
# Set the browser size to 1280x1024
|
||||
world.browser.driver.set_window_size(1280, 1024)
|
||||
world.browser.driver.set_window_size(1280, 1024)
|
||||
|
||||
else:
|
||||
config = get_username_and_key()
|
||||
world.browser = Browser(
|
||||
'remote',
|
||||
url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']),
|
||||
**make_desired_capabilities()
|
||||
)
|
||||
world.browser.driver.implicitly_wait(30)
|
||||
|
||||
world.absorb(world.browser.driver.session_id, 'jobid')
|
||||
|
||||
|
||||
@before.each_scenario
|
||||
@@ -97,7 +151,6 @@ def clear_data(scenario):
|
||||
world.spew('scenario_dict')
|
||||
|
||||
|
||||
|
||||
@after.each_scenario
|
||||
def reset_databases(scenario):
|
||||
'''
|
||||
@@ -108,9 +161,10 @@ def reset_databases(scenario):
|
||||
mongo = MongoClient()
|
||||
mongo.drop_database(settings.CONTENTSTORE['OPTIONS']['db'])
|
||||
_CONTENTSTORE.clear()
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
|
||||
modulestore = xmodule.modulestore.django.editable_modulestore()
|
||||
modulestore.collection.drop()
|
||||
xmodule.modulestore.django._MODULESTORES.clear()
|
||||
xmodule.modulestore.django.clear_existing_modulestores()
|
||||
|
||||
|
||||
# Uncomment below to trigger a screenshot on error
|
||||
@@ -128,4 +182,6 @@ def teardown_browser(total):
|
||||
"""
|
||||
Quit the browser after executing the tests.
|
||||
"""
|
||||
if world.SAUCE_ENABLED:
|
||||
set_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed)
|
||||
world.browser.quit()
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.contrib.auth import authenticate, login
|
||||
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from student.models import CourseEnrollment
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import editable_modulestore
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from urllib import quote_plus
|
||||
|
||||
@@ -60,11 +60,9 @@ def register_by_course_id(course_id, is_staff=False):
|
||||
@world.absorb
|
||||
def clear_courses():
|
||||
# Flush and initialize the module store
|
||||
# It needs the templates because it creates new records
|
||||
# by cloning from the template.
|
||||
# Note that if your test module gets in some weird state
|
||||
# (though it shouldn't), do this manually
|
||||
# from the bash shell to drop it:
|
||||
# $ mongo test_xmodule --eval "db.dropDatabase()"
|
||||
modulestore().collection.drop()
|
||||
editable_modulestore().collection.drop()
|
||||
contentstore().fs_files.drop()
|
||||
|
||||
@@ -99,7 +99,7 @@ def i_am_logged_in_user(step):
|
||||
|
||||
@step('I am not logged in$')
|
||||
def i_am_not_logged_in(step):
|
||||
world.browser.cookies.delete()
|
||||
world.visit('logout')
|
||||
|
||||
|
||||
@step('I am staff for course "([^"]*)"$')
|
||||
@@ -138,10 +138,13 @@ def should_have_link_with_path_and_text(step, path, text):
|
||||
|
||||
@step(r'should( not)? see "(.*)" (?:somewhere|anywhere) (?:in|on) (?:the|this) page')
|
||||
def should_see_in_the_page(step, doesnt_appear, text):
|
||||
multiplier = 1
|
||||
if world.SAUCE_ENABLED:
|
||||
multiplier = 2
|
||||
if doesnt_appear:
|
||||
assert world.browser.is_text_not_present(text, wait_time=5)
|
||||
assert world.browser.is_text_not_present(text, wait_time=5*multiplier)
|
||||
else:
|
||||
assert world.browser.is_text_present(text, wait_time=5)
|
||||
assert world.browser.is_text_present(text, wait_time=5*multiplier)
|
||||
|
||||
|
||||
@step('I am logged in$')
|
||||
@@ -150,7 +153,7 @@ def i_am_logged_in(step):
|
||||
world.log_in(username='robot', password='test')
|
||||
world.browser.visit(django_url('/'))
|
||||
# You should not see the login link
|
||||
assert_equals(world.browser.find_by_css('a#login'), [])
|
||||
assert world.is_css_not_present('a#login')
|
||||
|
||||
|
||||
@step(u'I am an edX user$')
|
||||
|
||||
@@ -76,7 +76,7 @@ def replace_course_urls(get_html, course_id):
|
||||
return _get_html
|
||||
|
||||
|
||||
def replace_static_urls(get_html, data_dir, course_id=None):
|
||||
def replace_static_urls(get_html, data_dir, course_id=None, static_asset_path=''):
|
||||
"""
|
||||
Updates the supplied module with a new get_html function that wraps
|
||||
the old get_html function and substitutes urls of the form /static/...
|
||||
@@ -85,7 +85,7 @@ def replace_static_urls(get_html, data_dir, course_id=None):
|
||||
|
||||
@wraps(get_html)
|
||||
def _get_html():
|
||||
return static_replace.replace_static_urls(get_html(), data_dir, course_id)
|
||||
return static_replace.replace_static_urls(get_html(), data_dir, course_id, static_asset_path=static_asset_path)
|
||||
return _get_html
|
||||
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ class CombinedOpenEndedFields(object):
|
||||
help="The number of times the student can try to answer this problem.",
|
||||
default=1,
|
||||
scope=Scope.settings,
|
||||
values={"min" : 1 }
|
||||
values={"min": 1 }
|
||||
)
|
||||
accept_file_upload = Boolean(
|
||||
display_name="Allow File Uploads",
|
||||
@@ -229,12 +229,10 @@ class CombinedOpenEndedFields(object):
|
||||
)
|
||||
due = Date(
|
||||
help="Date that this problem is due by",
|
||||
default=None,
|
||||
scope=Scope.settings
|
||||
)
|
||||
graceperiod = Timedelta(
|
||||
help="Amount of time after the due date that submissions will be accepted",
|
||||
default=None,
|
||||
scope=Scope.settings
|
||||
)
|
||||
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
@@ -244,7 +242,7 @@ class CombinedOpenEndedFields(object):
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings,
|
||||
values={"min" : 0 , "step": ".1"},
|
||||
values={"min": 0, "step": ".1"},
|
||||
default=1
|
||||
)
|
||||
min_to_calibrate = Integer(
|
||||
@@ -252,28 +250,28 @@ class CombinedOpenEndedFields(object):
|
||||
help="The minimum number of calibration essays each student will need to complete for peer grading.",
|
||||
default=3,
|
||||
scope=Scope.settings,
|
||||
values={"min" : 1, "max" : 20, "step" : "1"}
|
||||
values={"min": 1, "max": 20, "step": "1"}
|
||||
)
|
||||
max_to_calibrate = Integer(
|
||||
display_name="Maximum Peer Grading Calibrations",
|
||||
help="The maximum number of calibration essays each student will need to complete for peer grading.",
|
||||
default=6,
|
||||
scope=Scope.settings,
|
||||
values={"min" : 1, "max" : 20, "step" : "1"}
|
||||
values={"min": 1, "max": 20, "step": "1"}
|
||||
)
|
||||
peer_grader_count = Integer(
|
||||
display_name="Peer Graders per Response",
|
||||
help="The number of peers who will grade each submission.",
|
||||
default=3,
|
||||
scope=Scope.settings,
|
||||
values={"min" : 1, "step" : "1", "max" : 5}
|
||||
values={"min": 1, "step": "1", "max": 5}
|
||||
)
|
||||
required_peer_grading = Integer(
|
||||
display_name="Required Peer Grading",
|
||||
help="The number of other students each student making a submission will have to grade.",
|
||||
default=3,
|
||||
scope=Scope.settings,
|
||||
values={"min" : 1, "step" : "1", "max" : 5}
|
||||
values={"min": 1, "step": "1", "max": 5}
|
||||
)
|
||||
markdown = String(
|
||||
help="Markdown source of this module",
|
||||
|
||||
@@ -147,51 +147,51 @@ class TextbookList(List):
|
||||
|
||||
class CourseFields(object):
|
||||
textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course",
|
||||
default=[], scope=Scope.content)
|
||||
default=[], scope=Scope.content)
|
||||
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
|
||||
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
|
||||
enrollment_end = Date(help="Date that enrollment for this class is closed", scope=Scope.settings)
|
||||
start = Date(help="Start time when this module is visible",
|
||||
# using now(UTC()) resulted in fractional seconds which screwed up comparisons and anyway w/b the
|
||||
# time of first invocation of this stmt on the server
|
||||
default=datetime.fromtimestamp(0, UTC()),
|
||||
scope=Scope.settings)
|
||||
# using now(UTC()) resulted in fractional seconds which screwed up comparisons and anyway w/b the
|
||||
# time of first invocation of this stmt on the server
|
||||
default=datetime.fromtimestamp(0, UTC()),
|
||||
scope=Scope.settings)
|
||||
end = Date(help="Date that this class ends", scope=Scope.settings)
|
||||
advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings)
|
||||
grading_policy = Dict(help="Grading policy definition for this class",
|
||||
default={"GRADER": [
|
||||
{
|
||||
"type": "Homework",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"short_label": "HW",
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type": "Lab",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type": "Midterm Exam",
|
||||
"short_label": "Midterm",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.3
|
||||
},
|
||||
{
|
||||
"type": "Final Exam",
|
||||
"short_label": "Final",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS": {
|
||||
"Pass": 0.5
|
||||
}},
|
||||
scope=Scope.content)
|
||||
default={"GRADER": [
|
||||
{
|
||||
"type": "Homework",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"short_label": "HW",
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type": "Lab",
|
||||
"min_count": 12,
|
||||
"drop_count": 2,
|
||||
"weight": 0.15
|
||||
},
|
||||
{
|
||||
"type": "Midterm Exam",
|
||||
"short_label": "Midterm",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.3
|
||||
},
|
||||
{
|
||||
"type": "Final Exam",
|
||||
"short_label": "Final",
|
||||
"min_count": 1,
|
||||
"drop_count": 0,
|
||||
"weight": 0.4
|
||||
}
|
||||
],
|
||||
"GRADE_CUTOFFS": {
|
||||
"Pass": 0.5
|
||||
}},
|
||||
scope=Scope.content)
|
||||
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
|
||||
display_name = String(help="Display name for this module", default="Empty", display_name="Display Name", scope=Scope.settings)
|
||||
show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings)
|
||||
@@ -201,7 +201,7 @@ class CourseFields(object):
|
||||
discussion_topics = Dict(
|
||||
help="Map of topics names to ids",
|
||||
scope=Scope.settings
|
||||
)
|
||||
)
|
||||
testcenter_info = Dict(help="Dictionary of Test Center info", scope=Scope.settings)
|
||||
announcement = Date(help="Date this course is announced", scope=Scope.settings)
|
||||
cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings)
|
||||
@@ -216,128 +216,124 @@ class CourseFields(object):
|
||||
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
|
||||
has_children = True
|
||||
checklists = List(scope=Scope.settings,
|
||||
default=[
|
||||
{"short_description" : "Getting Started With Studio",
|
||||
"items" : [{"short_description": "Add Course Team Members",
|
||||
"long_description": "Grant your collaborators permission to edit your course so you can work together.",
|
||||
"is_checked": False,
|
||||
"action_url": "ManageUsers",
|
||||
"action_text": "Edit Course Team",
|
||||
"action_external": False},
|
||||
{"short_description": "Set Important Dates for Your Course",
|
||||
"long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Details & Schedule",
|
||||
"action_external": False},
|
||||
{"short_description": "Draft Your Course's Grading Policy",
|
||||
"long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsGrading",
|
||||
"action_text": "Edit Grading Settings",
|
||||
"action_external": False},
|
||||
{"short_description": "Explore the Other Studio Checklists",
|
||||
"long_description": "Discover other available course authoring tools, and find help when you need it.",
|
||||
"is_checked": False,
|
||||
"action_url": "",
|
||||
"action_text": "",
|
||||
"action_external": False}]
|
||||
},
|
||||
{"short_description" : "Draft a Rough Course Outline",
|
||||
"items" : [{"short_description": "Create Your First Section and Subsection",
|
||||
"long_description": "Use your course outline to build your first Section and Subsection.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Set Section Release Dates",
|
||||
"long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Designate a Subsection as Graded",
|
||||
"long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Reordering Course Content",
|
||||
"long_description": "Use drag and drop to reorder the content in your course.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Renaming Sections",
|
||||
"long_description": "Rename Sections by clicking the Section name from the Course Outline.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Deleting Course Content",
|
||||
"long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Add an Instructor-Only Section to Your Outline",
|
||||
"long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False}]
|
||||
},
|
||||
{"short_description" : "Explore edX's Support Tools",
|
||||
"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.",
|
||||
"is_checked": False,
|
||||
"action_url": "http://help.edge.edx.org/",
|
||||
"action_text": "Visit Studio Help",
|
||||
"action_external": True},
|
||||
{"short_description": "Enroll in edX 101",
|
||||
"long_description": "Register for edX 101, edX's primer for course creation.",
|
||||
"is_checked": False,
|
||||
"action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
|
||||
"action_text": "Register for edX 101",
|
||||
"action_external": True},
|
||||
{"short_description": "Download the Studio Documentation",
|
||||
"long_description": "Download the searchable Studio reference documentation in PDF form.",
|
||||
"is_checked": False,
|
||||
"action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf",
|
||||
"action_text": "Download Documentation",
|
||||
"action_external": True}]
|
||||
},
|
||||
{"short_description" : "Draft Your Course About Page",
|
||||
"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.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False},
|
||||
{"short_description": "Add Staff Bios",
|
||||
"long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False},
|
||||
{"short_description": "Add Course FAQs",
|
||||
"long_description": "Include a short list of frequently asked questions about your course.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False},
|
||||
{"short_description": "Add Course Prerequisites",
|
||||
"long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False}]
|
||||
}
|
||||
default=[
|
||||
{"short_description": "Getting Started With Studio",
|
||||
"items": [{"short_description": "Add Course Team Members",
|
||||
"long_description": "Grant your collaborators permission to edit your course so you can work together.",
|
||||
"is_checked": False,
|
||||
"action_url": "ManageUsers",
|
||||
"action_text": "Edit Course Team",
|
||||
"action_external": False},
|
||||
{"short_description": "Set Important Dates for Your Course",
|
||||
"long_description": "Establish your course's student enrollment and launch dates on the Schedule and Details page.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Details & Schedule",
|
||||
"action_external": False},
|
||||
{"short_description": "Draft Your Course's Grading Policy",
|
||||
"long_description": "Set up your assignment types and grading policy even if you haven't created all your assignments.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsGrading",
|
||||
"action_text": "Edit Grading Settings",
|
||||
"action_external": False},
|
||||
{"short_description": "Explore the Other Studio Checklists",
|
||||
"long_description": "Discover other available course authoring tools, and find help when you need it.",
|
||||
"is_checked": False,
|
||||
"action_url": "",
|
||||
"action_text": "",
|
||||
"action_external": False}]},
|
||||
{"short_description": "Draft a Rough Course Outline",
|
||||
"items": [{"short_description": "Create Your First Section and Subsection",
|
||||
"long_description": "Use your course outline to build your first Section and Subsection.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Set Section Release Dates",
|
||||
"long_description": "Specify the release dates for each Section in your course. Sections become visible to students on their release dates.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Designate a Subsection as Graded",
|
||||
"long_description": "Set a Subsection to be graded as a specific assignment type. Assignments within graded Subsections count toward a student's final grade.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Reordering Course Content",
|
||||
"long_description": "Use drag and drop to reorder the content in your course.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Renaming Sections",
|
||||
"long_description": "Rename Sections by clicking the Section name from the Course Outline.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Deleting Course Content",
|
||||
"long_description": "Delete Sections, Subsections, or Units you don't need anymore. Be careful, as there is no Undo function.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False},
|
||||
{"short_description": "Add an Instructor-Only Section to Your Outline",
|
||||
"long_description": "Some course authors find using a section for unsorted, in-progress work useful. To do this, create a section and set the release date to the distant future.",
|
||||
"is_checked": False,
|
||||
"action_url": "CourseOutline",
|
||||
"action_text": "Edit Course Outline",
|
||||
"action_external": False}]},
|
||||
{"short_description": "Explore edX's Support Tools",
|
||||
"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.",
|
||||
"is_checked": False,
|
||||
"action_url": "http://help.edge.edx.org/",
|
||||
"action_text": "Visit Studio Help",
|
||||
"action_external": True},
|
||||
{"short_description": "Enroll in edX 101",
|
||||
"long_description": "Register for edX 101, edX's primer for course creation.",
|
||||
"is_checked": False,
|
||||
"action_url": "https://edge.edx.org/courses/edX/edX101/How_to_Create_an_edX_Course/about",
|
||||
"action_text": "Register for edX 101",
|
||||
"action_external": True},
|
||||
{"short_description": "Download the Studio Documentation",
|
||||
"long_description": "Download the searchable Studio reference documentation in PDF form.",
|
||||
"is_checked": False,
|
||||
"action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf",
|
||||
"action_text": "Download Documentation",
|
||||
"action_external": True}]},
|
||||
{"short_description": "Draft Your Course About Page",
|
||||
"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.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False},
|
||||
{"short_description": "Add Staff Bios",
|
||||
"long_description": "Showing prospective students who their instructor will be is helpful. Include staff bios on the course About page.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False},
|
||||
{"short_description": "Add Course FAQs",
|
||||
"long_description": "Include a short list of frequently asked questions about your course.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False},
|
||||
{"short_description": "Add Course Prerequisites",
|
||||
"long_description": "Let students know what knowledge and/or skills they should have before they enroll in your course.",
|
||||
"is_checked": False,
|
||||
"action_url": "SettingsDetails",
|
||||
"action_text": "Edit Course Schedule & Details",
|
||||
"action_external": False}]}
|
||||
])
|
||||
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)
|
||||
enrollment_domain = String(help="External login method associated with user accounts allowed to register in course",
|
||||
scope=Scope.settings)
|
||||
scope=Scope.settings)
|
||||
course_image = String(
|
||||
help="Filename of the course image",
|
||||
scope=Scope.settings,
|
||||
|
||||
@@ -19,10 +19,10 @@ h2 {
|
||||
|
||||
|
||||
iframe[seamless]{
|
||||
background-color: transparent;
|
||||
border: 0px none transparent;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
overflow: hidden;
|
||||
padding: 0px;
|
||||
border: 0px none transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.inline-error {
|
||||
@@ -31,17 +31,17 @@ iframe[seamless]{
|
||||
|
||||
section.problem-progress {
|
||||
display: inline-block;
|
||||
color: #999;
|
||||
font-size: em(16);
|
||||
font-weight: 100;
|
||||
padding-left: 5px;
|
||||
color: #999;
|
||||
font-weight: 100;
|
||||
font-size: em(16);
|
||||
}
|
||||
|
||||
section.problem {
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
|
||||
canvas, img {
|
||||
page-break-inside: avoid;
|
||||
@@ -49,30 +49,29 @@ section.problem {
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.choicegroup {
|
||||
@include clearfix;
|
||||
|
||||
label.choicegroup_correct{
|
||||
&:after{
|
||||
content: url('../images/correct-icon.png');
|
||||
margin-left:15px
|
||||
}
|
||||
}
|
||||
|
||||
label.choicegroup_incorrect{
|
||||
&:after{
|
||||
content: url('../images/incorrect-icon.png');
|
||||
margin-left:15px;
|
||||
}
|
||||
}
|
||||
|
||||
min-width:100px;
|
||||
min-width: 100px;
|
||||
width: auto !important;
|
||||
width: 100px;
|
||||
|
||||
label.choicegroup_correct {
|
||||
&:after {
|
||||
margin-left: 15px;
|
||||
content: url('../images/correct-icon.png');
|
||||
}
|
||||
}
|
||||
|
||||
label.choicegroup_incorrect {
|
||||
&:after {
|
||||
margin-left: 15px;
|
||||
content: url('../images/incorrect-icon.png');
|
||||
}
|
||||
}
|
||||
|
||||
.indicator_container {
|
||||
float: left;
|
||||
width: 25px;
|
||||
@@ -82,9 +81,9 @@ section.problem {
|
||||
|
||||
fieldset {
|
||||
@include box-sizing(border-box);
|
||||
margin: 0px 0px $baseline;
|
||||
padding-left: $baseline;
|
||||
border-left: 1px solid #ddd;
|
||||
padding-left: 20px;
|
||||
margin: 0px 0px 20px;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
@@ -102,21 +101,21 @@ section.problem {
|
||||
ol.enumerate {
|
||||
li {
|
||||
&:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.solution-span {
|
||||
> span {
|
||||
margin: 20px 0;
|
||||
margin: $baseline 0;
|
||||
display: block;
|
||||
border: 1px solid #ddd;
|
||||
padding: 9px 15px 20px;
|
||||
background: #FFF;
|
||||
padding: 9px 15px $baseline;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
box-shadow: inset 0 0 0 1px #eee;
|
||||
border-radius: 3px;
|
||||
@@ -133,26 +132,26 @@ section.problem {
|
||||
margin-top: -2px;
|
||||
}
|
||||
&.status {
|
||||
margin: 8px 0 0 $baseline/2;
|
||||
text-indent: -9999px;
|
||||
margin: 8px 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.unanswered {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -163,9 +162,9 @@ section.problem {
|
||||
&.processing {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -176,9 +175,9 @@ section.problem {
|
||||
&.incorrect, &.incomplete, &.ui-icon-close {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
@@ -195,12 +194,12 @@ section.problem {
|
||||
p.answer {
|
||||
@include inline-block();
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
margin-left: $baseline/2;
|
||||
|
||||
&:before {
|
||||
display: inline;
|
||||
content: "Answer: ";
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
|
||||
}
|
||||
&:empty {
|
||||
@@ -228,12 +227,12 @@ section.problem {
|
||||
margin-bottom: 0;
|
||||
|
||||
&.math {
|
||||
padding: 6px;
|
||||
background: #f1f1f1;
|
||||
border: 1px solid #e3e3e3;
|
||||
@include inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
min-width: 30px;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 4px;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,98 +240,91 @@ section.problem {
|
||||
span {
|
||||
&.unanswered, &.ui-icon-bullet {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
&.processing, &.ui-icon-processing {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
&.partially-correct {
|
||||
@include inline-block();
|
||||
background: url('../images/partially-correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/partially-correct-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
&.incorrect, &.incomplete, &.ui-icon-close {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.reload
|
||||
{
|
||||
.reload {
|
||||
float:right;
|
||||
margin: 10px;
|
||||
margin: $baseline/2;
|
||||
}
|
||||
|
||||
|
||||
.grader-status {
|
||||
padding: 9px;
|
||||
background: #F6F6F6;
|
||||
border: 1px solid #ddd;
|
||||
border-top: 0;
|
||||
margin-bottom: 20px;
|
||||
@include clearfix;
|
||||
margin: $baseline/2 0;
|
||||
padding: $baseline/2;
|
||||
border-radius: 5px;
|
||||
background: #F6F6F6;
|
||||
|
||||
span {
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
margin: -7px 7px 0 0;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
.grading {
|
||||
background: url('../images/info-icon.png') left center no-repeat;
|
||||
padding-left: 25px;
|
||||
text-indent: 0px;
|
||||
margin: 0px 7px 0 0;
|
||||
padding-left: 25px;
|
||||
background: url('../images/info-icon.png') left center no-repeat;
|
||||
text-indent: 0px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 20px;
|
||||
text-transform: capitalize;
|
||||
margin-bottom: 0;
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
text-transform: capitalize;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&.file {
|
||||
background: #FFF;
|
||||
margin-top: 20px;
|
||||
padding: 20px 0 0 0;
|
||||
|
||||
border: {
|
||||
top: 1px solid #eee;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
margin-top: $baseline;
|
||||
padding: $baseline 0 0 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
background: #fff;
|
||||
|
||||
p.debug {
|
||||
display: none;
|
||||
@@ -345,54 +337,54 @@ section.problem {
|
||||
|
||||
}
|
||||
.evaluation {
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.feedback-on-feedback {
|
||||
height: 100px;
|
||||
margin-right: 20px;
|
||||
margin-right: $baseline;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.evaluation-response {
|
||||
header {
|
||||
text-align: right;
|
||||
a {
|
||||
font-size: .85em;
|
||||
}
|
||||
header {
|
||||
text-align: right;
|
||||
a {
|
||||
font-size: .85em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.evaluation-scoring {
|
||||
.scoring-list {
|
||||
list-style-type: none;
|
||||
margin-left: 3px;
|
||||
.scoring-list {
|
||||
margin-left: 3px;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
display:inline;
|
||||
margin-left: 50px;
|
||||
li {
|
||||
display:inline;
|
||||
margin-left: 50px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
}
|
||||
label {
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.submit-message-container {
|
||||
margin: 10px 0px ;
|
||||
margin: $baseline 0px ;
|
||||
}
|
||||
}
|
||||
|
||||
form.option-input {
|
||||
margin: -10px 0 20px;
|
||||
padding-bottom: 20px;
|
||||
margin: -$baseline/2 0 $baseline;
|
||||
padding-bottom: $baseline;
|
||||
|
||||
select {
|
||||
margin-right: flex-gutter();
|
||||
@@ -400,17 +392,17 @@ section.problem {
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
list-style: disc outside none;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
list-style: decimal outside none;
|
||||
}
|
||||
|
||||
dl {
|
||||
@@ -431,8 +423,8 @@ section.problem {
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 1.4em;
|
||||
margin-bottom: lh(.5);
|
||||
line-height: 1.4em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -449,8 +441,8 @@ section.problem {
|
||||
table-layout: auto;
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td {
|
||||
@@ -463,44 +455,43 @@ section.problem {
|
||||
}
|
||||
|
||||
caption {
|
||||
background: #f1f1f1;
|
||||
margin-bottom: .75em;
|
||||
margin-bottom: .75rem;
|
||||
padding: .75em 0;
|
||||
padding: .75rem 0;
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
tr, td, th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0 2px;
|
||||
padding: 0px 5px;
|
||||
white-space: nowrap;
|
||||
border: 1px solid #EAEAEA;
|
||||
background-color: #F8F8F8;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 3px;
|
||||
background-color: #f8f8f8;
|
||||
white-space: nowrap;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #F8F8F8;
|
||||
border: 1px solid #CCC;
|
||||
overflow: auto;
|
||||
padding: 6px $baseline/2;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
background-color: #f8f8f8;
|
||||
font-size: .9em;
|
||||
line-height: 1.4;
|
||||
overflow: auto;
|
||||
padding: 6px 10px;
|
||||
border-radius: 3px;
|
||||
|
||||
> code {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: pre;
|
||||
border: none;
|
||||
background: transparent;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,26 +508,25 @@ section.problem {
|
||||
}
|
||||
|
||||
pre {
|
||||
border-radius: 0;
|
||||
border-radius: 0;
|
||||
border-width: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-width: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
overflow: hidden;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
resize: none;
|
||||
|
||||
&.CodeMirror-cursor {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
visibility: hidden;
|
||||
border-left: 1px solid black;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
border-right: none;
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,14 +536,14 @@ section.problem {
|
||||
}
|
||||
|
||||
hr {
|
||||
background: #ddd;
|
||||
border: none;
|
||||
clear: both;
|
||||
color: #ddd;
|
||||
float: none;
|
||||
height: 1px;
|
||||
clear: both;
|
||||
margin: 0 0 .75rem;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border: none;
|
||||
background: #ddd;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@@ -570,17 +560,17 @@ section.problem {
|
||||
center {
|
||||
display: block;
|
||||
margin: lh() 0;
|
||||
border: 1px solid #ccc;
|
||||
padding: lh();
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
section.action {
|
||||
margin-top: 20px;
|
||||
margin-top: $baseline;
|
||||
|
||||
.save, .check, .show, .reset {
|
||||
height: ($baseline*2);
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.save {
|
||||
@@ -590,8 +580,8 @@ section.problem {
|
||||
.show {
|
||||
|
||||
.show-label {
|
||||
font-size: 1.0em;
|
||||
font-weight: 600;
|
||||
font-size: 1.0em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,20 +592,20 @@ section.problem {
|
||||
// padding: 8px 12px;
|
||||
// margin-top: 10px;
|
||||
@include inline-block;
|
||||
font-style: italic;
|
||||
margin: 8px 0 0 10px;
|
||||
margin: 8px 0 0 $baseline/2;
|
||||
color: #777;
|
||||
font-style: italic;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-solution {
|
||||
> p:first-child {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
text-transform: uppercase;
|
||||
color: #AAA;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
@@ -624,12 +614,12 @@ section.problem {
|
||||
}
|
||||
|
||||
div.capa_alert {
|
||||
margin-top: $baseline;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #EBE8BF;
|
||||
border: 1px solid #ebe8bf;
|
||||
border-radius: 3px;
|
||||
background: #FFFCDD;
|
||||
background: #fffcdd;
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
div.capa_reset {
|
||||
@@ -638,12 +628,14 @@ section.problem {
|
||||
background-color: lighten($error-red, 25%);
|
||||
border-radius: 3px;
|
||||
font-size: 1em;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: $baseline/2;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.capa_reset>h2 {
|
||||
color: #AA0000;
|
||||
color: #aa0000;
|
||||
}
|
||||
|
||||
.capa_reset li {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -652,10 +644,10 @@ section.problem {
|
||||
border: 1px solid #ccc;
|
||||
|
||||
h3 {
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
padding: 9px;
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
background: #eee;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
font-weight: bold;
|
||||
font-size: em(16);
|
||||
}
|
||||
@@ -675,7 +667,7 @@ section.problem {
|
||||
a {
|
||||
display: block;
|
||||
padding: 9px;
|
||||
background: #F6F6F6;
|
||||
background: #f6f6f6;
|
||||
box-shadow: inset 0 0 0 1px #fff;
|
||||
}
|
||||
}
|
||||
@@ -693,22 +685,22 @@ section.problem {
|
||||
margin-bottom: 12px;
|
||||
|
||||
h3 {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
text-transform: uppercase;
|
||||
color: #AAA;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
> section {
|
||||
border: 1px solid #ddd;
|
||||
padding: 9px 9px 20px;
|
||||
margin-bottom: 10px;
|
||||
background: #FFF;
|
||||
position: relative;
|
||||
box-shadow: inset 0 0 0 1px #eee;
|
||||
margin-bottom: $baseline/2;
|
||||
padding: 9px 9px $baseline;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px #eee;
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
@@ -719,28 +711,29 @@ section.problem {
|
||||
}
|
||||
|
||||
a.full {
|
||||
@include position(absolute, 0 0 1px 0px);
|
||||
font-size: .8em;
|
||||
padding: 4px;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
display: block;
|
||||
background: #F3F3F3;
|
||||
@include position(absolute, 0 0 1px 0);
|
||||
@include box-sizing(border-box);
|
||||
display: block;
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
background: #f3f3f3;
|
||||
text-align: right;
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.external-grader-message {
|
||||
section {
|
||||
padding-left: 20px;
|
||||
background-color: #FAFAFA;
|
||||
color: #2C2C2C;
|
||||
font-family: monospace;
|
||||
padding-top: $baseline/2;
|
||||
padding-left: $baseline;
|
||||
background-color: #fafafa;
|
||||
color: #2c2c2c;
|
||||
font-size: 1em;
|
||||
padding-top: 10px;
|
||||
font-family: monospace;
|
||||
|
||||
header {
|
||||
font-size: 1.4em;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.shortform {
|
||||
@@ -748,35 +741,36 @@ section.problem {
|
||||
}
|
||||
|
||||
.longform {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.result-errors {
|
||||
margin: 5px;
|
||||
padding: 10px 10px 10px 40px;
|
||||
margin: $baseline/4;
|
||||
padding: $baseline/2 $baseline/2 $baseline/2 $baseline*2;
|
||||
background: url('../images/incorrect-icon.png') center left no-repeat;
|
||||
|
||||
li {
|
||||
color: #B00;
|
||||
}
|
||||
color: #b00;
|
||||
}
|
||||
}
|
||||
|
||||
.result-output {
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
margin: $baseline/4;
|
||||
padding: $baseline 0 15px 50px;
|
||||
border-top: 1px solid #ddd;
|
||||
border-left: $baseline solid #fafafa;
|
||||
|
||||
h4 {
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin-top: 20px;
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
dd {
|
||||
@@ -786,6 +780,7 @@ section.problem {
|
||||
|
||||
.result-correct {
|
||||
background: url('../images/correct-icon.png') left 20px no-repeat;
|
||||
|
||||
.result-actual-output {
|
||||
color: #090;
|
||||
}
|
||||
@@ -793,6 +788,7 @@ section.problem {
|
||||
|
||||
.result-incorrect {
|
||||
background: url('../images/incorrect-icon.png') left 20px no-repeat;
|
||||
|
||||
.result-actual-output {
|
||||
color: #B00;
|
||||
}
|
||||
@@ -800,16 +796,16 @@ section.problem {
|
||||
|
||||
.markup-text{
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
padding: $baseline 0 15px 50px;
|
||||
border-top: 1px solid #ddd;
|
||||
border-left: 20px solid #fafafa;
|
||||
|
||||
bs {
|
||||
color: #BB0000;
|
||||
color: #bb0000;
|
||||
}
|
||||
|
||||
bg {
|
||||
color: #BDA046;
|
||||
color: #bda046;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -817,96 +813,111 @@ section.problem {
|
||||
}
|
||||
|
||||
.rubric {
|
||||
tr {
|
||||
margin:10px 0px;
|
||||
height: 100%;
|
||||
}
|
||||
td {
|
||||
padding: 20px 0px;
|
||||
margin: 10px 0px;
|
||||
height: 100%;
|
||||
}
|
||||
th {
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
label,
|
||||
.view-only {
|
||||
margin:3px;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
width: 150px;
|
||||
height:100%;
|
||||
display: inline-block;
|
||||
min-height: 50px;
|
||||
min-width: 50px;
|
||||
background-color: #CCC;
|
||||
font-size: .9em;
|
||||
}
|
||||
.grade {
|
||||
position: absolute;
|
||||
bottom:0px;
|
||||
right:0px;
|
||||
margin:10px;
|
||||
}
|
||||
.selected-grade {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
input[type=radio]:checked + label {
|
||||
background: #666;
|
||||
color: white; }
|
||||
input[class='score-selection'] {
|
||||
display: none;
|
||||
}
|
||||
tr {
|
||||
margin: $baseline/2 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
margin: $baseline/2 0;
|
||||
padding: $baseline 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
margin: $baseline/4;
|
||||
padding: $baseline/4;
|
||||
}
|
||||
|
||||
label,
|
||||
.view-only {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 3px;
|
||||
padding: 15px;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
background-color: #ccc;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.grade {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: $baseline/2;
|
||||
}
|
||||
|
||||
.selected-grade {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
|
||||
input[type=radio]:checked + label {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
|
||||
input[class='score-selection'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.annotation-input {
|
||||
$yellow: rgba(255,255,10,0.3);
|
||||
|
||||
margin: 0 0 1em 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 1em;
|
||||
margin: 0 0 1em 0;
|
||||
|
||||
.annotation-header {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: .5em 1em;
|
||||
border-bottom: 1px solid #ccc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.annotation-body { padding: .5em 1em; }
|
||||
|
||||
a.annotation-return {
|
||||
float: right;
|
||||
font: inherit;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a.annotation-return:after { content: " \2191" }
|
||||
|
||||
.block, ul.tags {
|
||||
margin: .5em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.block-highlight {
|
||||
padding: .5em;
|
||||
border: 1px solid darken($yellow, 10%);
|
||||
background-color: $yellow;
|
||||
color: #333;
|
||||
font-style: normal;
|
||||
background-color: $yellow;
|
||||
border: 1px solid darken($yellow, 10%);
|
||||
}
|
||||
|
||||
.block-comment { font-style: italic; }
|
||||
|
||||
ul.tags {
|
||||
display: block;
|
||||
list-style-type: none;
|
||||
margin-left: 1em;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 1em 0 0 0;
|
||||
position: relative;
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-left: $baseline*2;
|
||||
border: 1px solid rgb(102,102,102);
|
||||
margin-left: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background-color: $yellow;
|
||||
}
|
||||
@@ -918,42 +929,49 @@ section.problem {
|
||||
.tag-status, .tag { padding: .25em .5em; }
|
||||
}
|
||||
}
|
||||
|
||||
textarea.comment {
|
||||
$num-lines-to-show: 5;
|
||||
$line-height: 1.4em;
|
||||
$padding: .2em;
|
||||
width: 100%;
|
||||
padding: $padding (2 * $padding);
|
||||
line-height: $line-height;
|
||||
width: 100%;
|
||||
height: ($num-lines-to-show * $line-height) + (2*$padding) - (($line-height - 1)/2);
|
||||
line-height: $line-height;
|
||||
}
|
||||
|
||||
.answer-annotation { display: block; margin: 0; }
|
||||
|
||||
/* for debugging the input value field. enable the debug flag on the inputtype */
|
||||
.debug-value {
|
||||
color: #fff;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
background-color: #999;
|
||||
border: 1px solid #000;
|
||||
input[type="text"] { width: 100%; }
|
||||
pre { background-color: #CCC; color: #000; }
|
||||
&:before {
|
||||
display: block;
|
||||
content: "debug input value";
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
margin: 1em 0;
|
||||
padding: 1em;
|
||||
border: 1px solid #000;
|
||||
background-color: #999;
|
||||
color: #fff;
|
||||
|
||||
input[type="text"] { width: 100%; }
|
||||
|
||||
pre { background-color: #CCC; color: #000; }
|
||||
|
||||
&:before {
|
||||
display: block;
|
||||
content: "debug input value";
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.choicetextgroup{
|
||||
@extend .choicegroup;
|
||||
|
||||
input[type="text"]{
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
@extend .choicegroup;
|
||||
|
||||
label.choicetextgroup_correct, section.choicetextgroup_correct{
|
||||
label.choicetextgroup_correct, section.choicetextgroup_correct {
|
||||
@extend label.choicegroup_correct;
|
||||
|
||||
input[type="text"] {
|
||||
@@ -961,17 +979,18 @@ section.problem {
|
||||
}
|
||||
}
|
||||
|
||||
label.choicetextgroup_incorrect, section.choicetextgroup_incorrect{
|
||||
label.choicetextgroup_incorrect, section.choicetextgroup_incorrect {
|
||||
@extend label.choicegroup_incorrect;
|
||||
}
|
||||
|
||||
label.choicetextgroup_show_correct, section.choicetextgroup_show_correct{
|
||||
&:after{
|
||||
content: url('../images/correct-icon.png');
|
||||
label.choicetextgroup_show_correct, section.choicetextgroup_show_correct {
|
||||
&:after {
|
||||
margin-left:15px;
|
||||
content: url('../images/correct-icon.png');
|
||||
}
|
||||
}
|
||||
span.mock_label{
|
||||
|
||||
span.mock_label {
|
||||
cursor : default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// lms - xmodule - combinedopenended
|
||||
// ====================
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
@@ -16,244 +19,470 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
// Problem Header
|
||||
div.name{
|
||||
padding-bottom: 15px;
|
||||
|
||||
h2 {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: inline;
|
||||
float: right;
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-error {
|
||||
color: darken($error-red, 10%);
|
||||
}
|
||||
|
||||
section.combined-open-ended {
|
||||
@include clearfix;
|
||||
.status-container
|
||||
{
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
div.problemwrapper {
|
||||
border: 1px solid lightgray;
|
||||
border-radius: $baseline/2;
|
||||
|
||||
.status-bar {
|
||||
background-color: #eee;
|
||||
border-radius: $baseline/2 $baseline/2 0 0;
|
||||
border-bottom: 1px solid lightgray;
|
||||
|
||||
.statustable {
|
||||
width: 100%;
|
||||
padding: $baseline;
|
||||
}
|
||||
|
||||
.status-container {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
|
||||
.status-elements {
|
||||
border-radius: $baseline/4;
|
||||
border: 1px solid lightgray;
|
||||
}
|
||||
}
|
||||
|
||||
.problemtype-container {
|
||||
padding: $baseline/2;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.problemtype{
|
||||
padding: $baseline/2;
|
||||
}
|
||||
|
||||
.assessments-container {
|
||||
float: right;
|
||||
padding: $baseline/2 $baseline $baseline/2 $baseline/2;
|
||||
|
||||
.assessment-text {
|
||||
display: inline-block;
|
||||
display: table-cell;
|
||||
padding-right: $baseline/2;
|
||||
}
|
||||
}
|
||||
}
|
||||
.item-container
|
||||
{
|
||||
padding-bottom: 10px;
|
||||
.item-container {
|
||||
padding-bottom: $baseline/2;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.result-container
|
||||
{
|
||||
float:left;
|
||||
width: 100%;
|
||||
position:relative;
|
||||
}
|
||||
h4
|
||||
{
|
||||
margin-bottom:10px;
|
||||
.result-container {
|
||||
float: left;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
section.legend-container {
|
||||
margin: 15px;
|
||||
border-radius: $baseline/4;
|
||||
|
||||
.legenditem {
|
||||
background-color : #d4d4d4;
|
||||
font-size: .9em;
|
||||
padding: 2px;
|
||||
display: inline;
|
||||
padding: $baseline/2;
|
||||
width: 20%;
|
||||
background-color: #eee;
|
||||
font-size: .9em;
|
||||
}
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
section.combined-open-ended-status {
|
||||
vertical-align: center;
|
||||
|
||||
.statusitem {
|
||||
color: #2C2C2C;
|
||||
background-color : #d4d4d4;
|
||||
font-size: .9em;
|
||||
padding: 2px;
|
||||
display: inline;
|
||||
width: 20%;
|
||||
.show-results {
|
||||
margin-top: .3em;
|
||||
text-align:right;
|
||||
}
|
||||
.show-results-button {
|
||||
font: 1em monospace;
|
||||
}
|
||||
.statusitem {
|
||||
display: table-cell;
|
||||
padding: $baseline/2;
|
||||
width: 30px;
|
||||
border-right: 1px solid lightgray;
|
||||
background-color: #eee;
|
||||
color: #2c2c2c;
|
||||
font-size: .9em;
|
||||
|
||||
&:first-child {
|
||||
border-radius: $baseline/4 0 0 $baseline/4;
|
||||
}
|
||||
|
||||
.statusitem-current {
|
||||
background-color: #B2B2B2;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
span {
|
||||
&.unanswered {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
width: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.correct {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
width: 25px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
&.incorrect {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
border-radius: 0 $baseline/4 $baseline/4 0;
|
||||
}
|
||||
|
||||
&:only-child {
|
||||
border-radius: $baseline/4;
|
||||
}
|
||||
|
||||
.show-results {
|
||||
margin-top: .3em;
|
||||
text-align:right;
|
||||
}
|
||||
|
||||
.show-results-button {
|
||||
font: 1em monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.statusitem-current {
|
||||
background-color: #fff;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
span {
|
||||
&.unanswered {
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
float: right;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
&.correct {
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
float: right;
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
&.incorrect {
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
float: right;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-caret-right {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
// Problem Section Controls
|
||||
|
||||
.visibility-control, .visibility-control-prompt {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
.inner {
|
||||
float: left;
|
||||
margin-top: $baseline;
|
||||
width: 85%;
|
||||
height: 5px;
|
||||
border-top: 1px dotted #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: block;
|
||||
float: right;
|
||||
padding-top: $baseline/2;
|
||||
width: 15%;
|
||||
text-align: center;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
// Rubric Styling
|
||||
|
||||
.wrapper-score-selection {
|
||||
display: table-cell;
|
||||
padding: 0 $baseline/2;
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wrappable {
|
||||
display: table-cell;
|
||||
padding: $baseline/4;
|
||||
}
|
||||
|
||||
.rubric-list-item {
|
||||
margin-bottom: 2px;
|
||||
padding: $baseline/2;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
.rubric-label-selected{
|
||||
border-radius: $baseline/4;
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
span.rubric-category {
|
||||
display: block;
|
||||
margin-bottom: $baseline/2;
|
||||
padding-top: $baseline/2;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid lightgray;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
div.combined-rubric-container {
|
||||
ul.rubric-list{
|
||||
margin: 15px;
|
||||
padding-top: $baseline/2;
|
||||
padding-bottom: $baseline/4;
|
||||
|
||||
ul.rubric-list {
|
||||
margin: 0 $baseline $baseline/2 $baseline;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
padding:0;
|
||||
margin:0;
|
||||
|
||||
li {
|
||||
&.rubric-list-item{
|
||||
|
||||
&.rubric-list-item {
|
||||
margin-bottom: 2px;
|
||||
padding: 0px;
|
||||
padding: $baseline/2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding-top: $baseline/2;
|
||||
}
|
||||
|
||||
span.rubric-category {
|
||||
font-size: .9em;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid lightgray;
|
||||
font-weight: bold;
|
||||
font-size: .9em;
|
||||
}
|
||||
padding-bottom: 5px;
|
||||
padding-top: 10px;
|
||||
|
||||
label.choicegroup_correct {
|
||||
&:before {
|
||||
margin-right: 15px;
|
||||
content: url('../images/correct-icon.png');
|
||||
}
|
||||
}
|
||||
|
||||
label.choicegroup_partialcorrect {
|
||||
&:before {
|
||||
margin-right: 15px;
|
||||
content: url('../images/partially-correct-icon.png');
|
||||
}
|
||||
}
|
||||
|
||||
label.choicegroup_incorrect {
|
||||
&:before {
|
||||
margin-right: 15px;
|
||||
content: url('../images/incorrect-icon.png');
|
||||
}
|
||||
}
|
||||
|
||||
div.written-feedback {
|
||||
background: #f6f6f6;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
div.result-container {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 5px;
|
||||
.evaluation {
|
||||
padding-top: $baseline/2;
|
||||
padding-bottom: $baseline/4;
|
||||
|
||||
p {
|
||||
margin-bottom: 1px;
|
||||
.evaluation {
|
||||
p {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-on-feedback {
|
||||
height: 100px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.evaluation-response {
|
||||
margin-bottom: 2px;
|
||||
|
||||
header {
|
||||
a {
|
||||
font-size: .85em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.evaluation-scoring {
|
||||
.scoring-list {
|
||||
margin-left: 3px;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
display:inline;
|
||||
margin-left: 0;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-on-feedback {
|
||||
height: 100px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.evaluation-response {
|
||||
margin-bottom: 2px;
|
||||
header {
|
||||
a {
|
||||
font-size: .85em;
|
||||
}
|
||||
label {
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.evaluation-scoring {
|
||||
.scoring-list {
|
||||
list-style-type: none;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
li {
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
display:inline;
|
||||
margin-left: 0px;
|
||||
.submit-message-container {
|
||||
margin: $baseline/2 0;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
.external-grader-message {
|
||||
margin-bottom: $baseline/4;
|
||||
|
||||
section {
|
||||
padding-left: $baseline;
|
||||
background-color: #fafafa;
|
||||
color: #2c2c2c;
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
padding-top: $baseline/2;
|
||||
padding-bottom: 30px;
|
||||
|
||||
header {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.shortform {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.longform {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.result-errors {
|
||||
margin: $baseline/4;
|
||||
padding: $baseline/2 $baseline/2 $baseline/2 $baseline*2;
|
||||
background: url('../images/incorrect-icon.png') center left no-repeat;
|
||||
|
||||
li {
|
||||
color: #B00;
|
||||
}
|
||||
}
|
||||
}
|
||||
.submit-message-container {
|
||||
margin: 10px 0px ;
|
||||
}
|
||||
|
||||
.external-grader-message {
|
||||
margin-bottom: 5px;
|
||||
section {
|
||||
padding-left: 20px;
|
||||
background-color: #FAFAFA;
|
||||
color: #2C2C2C;
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
padding-top: 10px;
|
||||
padding-bottom:30px;
|
||||
header {
|
||||
font-size: 1.4em;
|
||||
.result-output {
|
||||
margin: $baseline/4;
|
||||
padding: $baseline 0 15px 50px;
|
||||
border-top: 1px solid #ddd;
|
||||
border-left: 20px solid #fafafa;
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.shortform {
|
||||
font-weight: bold;
|
||||
dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.longform {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
dt {
|
||||
margin-top: $baseline;
|
||||
}
|
||||
|
||||
.result-errors {
|
||||
margin: 5px;
|
||||
padding: 10px 10px 10px 40px;
|
||||
background: url('../images/incorrect-icon.png') center left no-repeat;
|
||||
li {
|
||||
color: #B00;
|
||||
}
|
||||
}
|
||||
dd {
|
||||
margin-left: 24pt;
|
||||
}
|
||||
}
|
||||
|
||||
.result-output {
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
.markup-text{
|
||||
margin: $baseline/4;
|
||||
padding: $baseline 0 15px 50px;
|
||||
border-top: 1px solid #ddd;
|
||||
border-left: 20px solid #fafafa;
|
||||
|
||||
h4 {
|
||||
font-family: monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
bs {
|
||||
color: #bb0000;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 24pt;
|
||||
}
|
||||
}
|
||||
|
||||
.markup-text{
|
||||
margin: 5px;
|
||||
padding: 20px 0px 15px 50px;
|
||||
border-top: 1px solid #DDD;
|
||||
border-left: 20px solid #FAFAFA;
|
||||
|
||||
bs {
|
||||
color: #BB0000;
|
||||
}
|
||||
|
||||
bg {
|
||||
color: #BDA046;
|
||||
}
|
||||
}
|
||||
bg {
|
||||
color: #bda046;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rubric-result-container {
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
display: inline;
|
||||
|
||||
.rubric-result {
|
||||
font-size: .9em;
|
||||
padding: 2px;
|
||||
display: inline-table;
|
||||
}
|
||||
padding: 2px;
|
||||
margin: 0px;
|
||||
display : inline;
|
||||
}
|
||||
}
|
||||
|
||||
div.rubric {
|
||||
ul.rubric-list{
|
||||
margin: 0 $baseline $baseline/2 $baseline;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
&.rubric-list-item {
|
||||
margin-bottom: 2px;
|
||||
padding: $baseline/2;
|
||||
border-radius: $baseline/4;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.wrapper-score-selection {
|
||||
display: table-cell;
|
||||
padding: 0 $baseline/2;
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wrappable {
|
||||
display: table-cell;
|
||||
padding: $baseline/4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span.rubric-category {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid lightgray;
|
||||
font-weight: bold;
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,8 +490,8 @@ div.result-container {
|
||||
section.open-ended-child {
|
||||
@media print {
|
||||
display: block;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
width: auto;
|
||||
|
||||
canvas, img {
|
||||
page-break-inside: avoid;
|
||||
@@ -270,30 +499,30 @@ section.open-ended-child {
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
ol.enumerate {
|
||||
li {
|
||||
&:before {
|
||||
content: " ";
|
||||
display: block;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
content: " ";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.solution-span {
|
||||
> span {
|
||||
margin: 20px 0;
|
||||
display: block;
|
||||
border: 1px solid #ddd;
|
||||
padding: 9px 15px 20px;
|
||||
background: #FFF;
|
||||
position: relative;
|
||||
box-shadow: inset 0 0 0 1px #eee;
|
||||
display: block;
|
||||
margin: $baseline 0;
|
||||
padding: 9px 15px $baseline;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px #eee;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
@@ -301,196 +530,190 @@ section.open-ended-child {
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
&.answer {
|
||||
margin-top: -2px;
|
||||
}
|
||||
&.status {
|
||||
text-indent: -9999px;
|
||||
margin: 8px 0 0 10px;
|
||||
}
|
||||
p {
|
||||
&.answer {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
div.unanswered {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
&.status {
|
||||
margin: 8px 0 0 $baseline/2;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
}
|
||||
|
||||
div.correct, div.ui-icon-check {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: green;
|
||||
}
|
||||
}
|
||||
|
||||
div.processing {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
div.incorrect, div.ui-icon-close {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
margin-bottom: lh(.5);
|
||||
}
|
||||
|
||||
p.answer {
|
||||
div.unanswered {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
div.correct, div.ui-icon-check {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: green;
|
||||
}
|
||||
}
|
||||
|
||||
div.processing {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
div.incorrect, div.ui-icon-close {
|
||||
p.status {
|
||||
@include inline-block();
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
margin-bottom: lh(.5);
|
||||
}
|
||||
|
||||
p.answer {
|
||||
@include inline-block();
|
||||
margin-bottom: 0;
|
||||
margin-left: $baseline/2;
|
||||
|
||||
&:before {
|
||||
content: "Answer: ";
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
|
||||
}
|
||||
&:empty {
|
||||
&:before {
|
||||
content: "Answer: ";
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
|
||||
}
|
||||
&:empty {
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
&.unanswered, &.ui-icon-bullet {
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
top: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
&.processing, &.ui-icon-processing {
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
height: 20px;
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
}
|
||||
|
||||
&.incorrect, &.ui-icon-close {
|
||||
@include inline-block();
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.reload {
|
||||
float:right;
|
||||
margin: $baseline/2;
|
||||
}
|
||||
|
||||
div.short-form-response {
|
||||
@include clearfix;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0;
|
||||
padding: $baseline/2;
|
||||
min-height: 20px;
|
||||
height: auto;
|
||||
border: 1px solid #ddd;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.grader-status {
|
||||
@include clearfix;
|
||||
margin: $baseline/2 0;
|
||||
padding: $baseline/2;
|
||||
border-radius: 5px;
|
||||
background: #f6f6f6;
|
||||
|
||||
span {
|
||||
&.unanswered, &.ui-icon-bullet {
|
||||
@include inline-block();
|
||||
background: url('../images/unanswered-icon.png') center center no-repeat;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
&.processing, &.ui-icon-processing {
|
||||
@include inline-block();
|
||||
background: url('../images/spinner.gif') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
&.correct, &.ui-icon-check {
|
||||
@include inline-block();
|
||||
background: url('../images/correct-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
&.incorrect, &.ui-icon-close {
|
||||
@include inline-block();
|
||||
background: url('../images/incorrect-icon.png') center center no-repeat;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
display: block;
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
margin: -7px 7px 0 0;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
.reload
|
||||
{
|
||||
float:right;
|
||||
margin: 10px;
|
||||
.grading {
|
||||
margin: 0 7px 0 0;
|
||||
padding-left: 25px;
|
||||
background: url('../images/info-icon.png') left center no-repeat;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
div.short-form-response {
|
||||
background: #F6F6F6;
|
||||
border: 1px solid #ddd;
|
||||
margin-bottom: 0px;
|
||||
overflow-y: auto;
|
||||
height: 200px;
|
||||
@include clearfix;
|
||||
}
|
||||
p {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.grader-status {
|
||||
padding: 9px;
|
||||
background: #F6F6F6;
|
||||
border: 1px solid #ddd;
|
||||
border-top: 0;
|
||||
margin-bottom: 20px;
|
||||
@include clearfix;
|
||||
&.file {
|
||||
margin-top: $baseline;
|
||||
padding: $baseline 0 0 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #eee;
|
||||
background: #fff;
|
||||
|
||||
span {
|
||||
text-indent: -9999px;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: -7px 7px 0 0;
|
||||
p.debug {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grading {
|
||||
background: url('../images/info-icon.png') left center no-repeat;
|
||||
padding-left: 25px;
|
||||
text-indent: 0px;
|
||||
margin: 0px 7px 0 0;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 20px;
|
||||
margin-bottom: 0;
|
||||
input {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&.file {
|
||||
background: #FFF;
|
||||
margin-top: 20px;
|
||||
padding: 20px 0 0 0;
|
||||
|
||||
border: {
|
||||
top: 1px solid #eee;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
p.debug {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
form.option-input {
|
||||
margin: -10px 0 20px;
|
||||
padding-bottom: 20px;
|
||||
margin: -$baseline/2 0 $baseline;
|
||||
padding-bottom: $baseline;
|
||||
|
||||
select {
|
||||
margin-right: flex-gutter();
|
||||
@@ -498,29 +721,31 @@ section.open-ended-child {
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
}
|
||||
|
||||
ul.rubric-list{
|
||||
list-style-type: none;
|
||||
padding:0;
|
||||
margin:0;
|
||||
li {
|
||||
&.rubric-list-item{
|
||||
margin-bottom: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
ul.rubric-list{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
&.rubric-list-item {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
border-radius: $baseline/4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal outside none;
|
||||
margin-bottom: lh();
|
||||
margin-left: .75em;
|
||||
margin-left: .75rem;
|
||||
list-style: decimal outside none;
|
||||
}
|
||||
|
||||
dl {
|
||||
@@ -541,8 +766,9 @@ section.open-ended-child {
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0px;
|
||||
padding: 0px;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -553,14 +779,14 @@ section.open-ended-child {
|
||||
}
|
||||
|
||||
hr {
|
||||
background: #ddd;
|
||||
border: none;
|
||||
clear: both;
|
||||
color: #ddd;
|
||||
float: none;
|
||||
height: 1px;
|
||||
clear: both;
|
||||
margin: 0 0 .75rem;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border: none;
|
||||
background: #ddd;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@@ -574,7 +800,7 @@ section.open-ended-child {
|
||||
}
|
||||
|
||||
section.action {
|
||||
margin-top: 20px;
|
||||
margin-top: $baseline;
|
||||
|
||||
input.save {
|
||||
@extend .blue-button !optional;
|
||||
@@ -582,20 +808,20 @@ section.open-ended-child {
|
||||
|
||||
.submission_feedback {
|
||||
@include inline-block;
|
||||
font-style: italic;
|
||||
margin: 8px 0 0 10px;
|
||||
margin: 8px 0 0 $baseline/2;
|
||||
color: #777;
|
||||
font-style: italic;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-solution {
|
||||
> p:first-child {
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
text-transform: uppercase;
|
||||
color: #AAA;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
@@ -605,56 +831,136 @@ section.open-ended-child {
|
||||
|
||||
div.open-ended-alert,
|
||||
.save_message {
|
||||
margin-top: $baseline/2;
|
||||
margin-bottom: $baseline/4;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #EBE8BF;
|
||||
border: 1px solid #ebe8bf;
|
||||
border-radius: 3px;
|
||||
background: #FFFCDD;
|
||||
background: #fffcdd;
|
||||
font-size: 0.9em;
|
||||
margin-top: 10px;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
|
||||
div.capa_reset {
|
||||
margin-top: $baseline/2;
|
||||
margin-bottom: $baseline/2;
|
||||
padding: 25px;
|
||||
border: 1px solid $error-red;
|
||||
background-color: lighten($error-red, 25%);
|
||||
border-radius: 3px;
|
||||
background-color: lighten($error-red, 25%);
|
||||
font-size: 1em;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.capa_reset>h2 {
|
||||
color: #AA0000;
|
||||
|
||||
.capa_reset > h2 {
|
||||
color: #aa0000;
|
||||
}
|
||||
|
||||
.capa_reset li {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.assessment-container {
|
||||
margin: 40px 0px 30px 0px;
|
||||
.scoring-container
|
||||
{
|
||||
p
|
||||
{
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
label {
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
display: inline-block;
|
||||
min-width: 50px;
|
||||
background-color: #CCC;
|
||||
text-size: 1.5em;
|
||||
}
|
||||
|
||||
input[type=radio]:checked + label {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
input[class='grade-selection'] {
|
||||
display: none;
|
||||
}
|
||||
margin: $baseline*2 0px 30px 0px;
|
||||
|
||||
.scoring-container {
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin: $baseline/2;
|
||||
padding: $baseline/4;
|
||||
min-width: 50px;
|
||||
background-color: #ccc;
|
||||
text-size: 1.5em;
|
||||
}
|
||||
|
||||
input[type=radio]:checked + label {
|
||||
background: #666;
|
||||
color: white;
|
||||
}
|
||||
|
||||
input[class='grade-selection'] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.prompt {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding: $baseline/2 0;
|
||||
}
|
||||
}
|
||||
|
||||
//OE Tool Area Styling
|
||||
|
||||
.oe-tools {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
|
||||
.oe-tools-label, .oe-tools-scores-label {
|
||||
display: inline-block;
|
||||
padding: $baseline/2;
|
||||
vertical-align: middle;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.rubric-button {
|
||||
padding: 8px $baseline/4;
|
||||
}
|
||||
|
||||
.rubric-previous-button {
|
||||
margin-right: $baseline/4;
|
||||
}
|
||||
|
||||
.rubric-next-button {
|
||||
margin-left: $baseline/4;
|
||||
}
|
||||
|
||||
.next-step-button {
|
||||
margin: $baseline/2;
|
||||
}
|
||||
.reset-button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
// Staff Grading
|
||||
.problem-list-container {
|
||||
margin: $baseline/2;
|
||||
|
||||
.instructions {
|
||||
padding-bottom: $baseline/2;
|
||||
}
|
||||
}
|
||||
|
||||
.staff-grading {
|
||||
|
||||
.breadcrumbs {
|
||||
padding: $baseline/10;
|
||||
background-color: #f6f6f6;
|
||||
border-radius: 5px;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.prompt-wrapper {
|
||||
padding-top: $baseline/2;
|
||||
|
||||
.meta-info-wrapper {
|
||||
padding: $baseline/2;
|
||||
border-radius: $baseline/4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.peer-grading-container{
|
||||
div.peer-grading{
|
||||
section.calibration-feedback {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,13 @@ class HtmlFields(object):
|
||||
|
||||
|
||||
class HtmlModule(HtmlFields, XModule):
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/html/display.coffee')
|
||||
]
|
||||
}
|
||||
js = {
|
||||
'coffee': [
|
||||
resource_string(__name__, 'js/src/javascript_loader.coffee'),
|
||||
resource_string(__name__, 'js/src/collapsible.coffee'),
|
||||
resource_string(__name__, 'js/src/html/display.coffee')
|
||||
]
|
||||
}
|
||||
js_module_name = "HTMLModule"
|
||||
css = {'scss': [resource_string(__name__, 'css/html/display.scss')]}
|
||||
|
||||
@@ -118,8 +120,10 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
# from .html
|
||||
# 'filename' in html pointers is a relative path
|
||||
# (not same as 'html/blah.html' when the pointer is in a directory itself)
|
||||
pointer_path = "{category}/{url_path}".format(category='html',
|
||||
url_path=name_to_pathname(location.name))
|
||||
pointer_path = "{category}/{url_path}".format(
|
||||
category='html',
|
||||
url_path=name_to_pathname(location.name)
|
||||
)
|
||||
base = path(pointer_path).dirname()
|
||||
# log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename))
|
||||
filepath = "{base}/{name}.html".format(base=base, name=filename)
|
||||
@@ -164,19 +168,16 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
# TODO (vshnayder): make export put things in the right places.
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''If the contents are valid xml, write them to filename.xml. Otherwise,
|
||||
write just <html filename="" [meta-attrs="..."]> to filename.xml, and the html
|
||||
''' Write <html filename="" [meta-attrs="..."]> to filename.xml, and the html
|
||||
string to filename.html.
|
||||
'''
|
||||
try:
|
||||
return etree.fromstring(self.data)
|
||||
except etree.XMLSyntaxError:
|
||||
pass
|
||||
|
||||
# Not proper format. Write html to file, return an empty tag
|
||||
# Write html to file, return an empty tag
|
||||
pathname = name_to_pathname(self.url_name)
|
||||
filepath = u'{category}/{pathname}.html'.format(category=self.category,
|
||||
pathname=pathname)
|
||||
filepath = u'{category}/{pathname}.html'.format(
|
||||
category=self.category,
|
||||
pathname=pathname
|
||||
)
|
||||
|
||||
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as filestream:
|
||||
@@ -190,6 +191,7 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
|
||||
elt.set("filename", relname)
|
||||
return elt
|
||||
|
||||
|
||||
class AboutFields(object):
|
||||
display_name = String(
|
||||
help="Display name for this module",
|
||||
@@ -202,12 +204,14 @@ class AboutFields(object):
|
||||
scope=Scope.content
|
||||
)
|
||||
|
||||
|
||||
class AboutModule(AboutFields, HtmlModule):
|
||||
"""
|
||||
Overriding defaults but otherwise treated as HtmlModule.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class AboutDescriptor(AboutFields, HtmlDescriptor):
|
||||
"""
|
||||
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
|
||||
@@ -216,6 +220,7 @@ class AboutDescriptor(AboutFields, HtmlDescriptor):
|
||||
template_dir_name = "about"
|
||||
module_class = AboutModule
|
||||
|
||||
|
||||
class StaticTabFields(object):
|
||||
"""
|
||||
The overrides for Static Tabs
|
||||
@@ -241,6 +246,7 @@ class StaticTabModule(StaticTabFields, HtmlModule):
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class StaticTabDescriptor(StaticTabFields, HtmlDescriptor):
|
||||
"""
|
||||
These pieces of course content are treated as HtmlModules but we need to overload where the templates are located
|
||||
|
||||
@@ -1,75 +1,80 @@
|
||||
<section class="course-content">
|
||||
<section class="xmodule_display xmodule_CombinedOpenEndedModule" data-type="CombinedOpenEnded">
|
||||
<section class="xmodule_display xmodule_CombinedOpenEndedModule" data-type="CombinedOpenEnded">
|
||||
<section id="combined-open-ended" class="combined-open-ended" data-ajax-url="/courses/MITx/6.002x/2012_Fall/modx/i4x://MITx/6.002x/combinedopenended/CombinedOE" data-allow_reset="False" data-state="assessing" data-task-count="2" data-task-number="1">
|
||||
|
||||
<h2>Problem 1</h2>
|
||||
<div class="status-container">
|
||||
<h4>Status</h4>
|
||||
<div class="status-elements">
|
||||
<section id="combined-open-ended-status" class="combined-open-ended-status">
|
||||
|
||||
<div class="statusitem" data-status-number="0">
|
||||
|
||||
Step 1 (Problem complete) : 1 / 1
|
||||
<h2>Problem 1</h2>
|
||||
<div class="status-container">
|
||||
<h4>Status</h4>
|
||||
<div class="status-elements">
|
||||
<section id="combined-open-ended-status" class="combined-open-ended-status">
|
||||
<div class="statusitem" data-status-number="0">
|
||||
Step 1 (Problem complete) : 1 / 1
|
||||
<span class="correct" id="status"></span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="statusitem statusitem-current" data-status-number="1">
|
||||
|
||||
Step 2 (Being scored) : None / 1
|
||||
</div>
|
||||
<div class="statusitem statusitem-current" data-status-number="1">
|
||||
Step 2 (Being scored) : None / 1
|
||||
<span class="grading" id="status"></span>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="item-container">
|
||||
<h4>Problem</h4>
|
||||
<div class="item-container">
|
||||
<h4>Problem</h4>
|
||||
<div class="problem-container">
|
||||
<div class="item"><section id="openended_open_ended" class="open-ended-child" data-state="assessing" data-child-type="openended"><div class="error"></div>
|
||||
<div class="prompt">
|
||||
|
||||
Some prompt.
|
||||
|
||||
</div>
|
||||
<textarea rows="30" cols="80" name="answer" class="answer short-form-response" id="input_open_ended" disabled="disabled">Test submission. Yaaaaaaay!</textarea><div class="message-wrapper"></div>
|
||||
<div class="grader-status">
|
||||
<span class="grading" id="status_open_ended">Submitted for grading.</span>
|
||||
|
||||
</div>
|
||||
|
||||
<input type="button" value="Submit assessment" class="submit-button" name="show" style="display: none;"><input name="skip" class="skip-button" type="button" value="Skip Post-Assessment" style="display: none;"><div class="open-ended-action"></div>
|
||||
|
||||
<span id="answer_open_ended"></span>
|
||||
</section></div>
|
||||
<div class="item">
|
||||
<section id="openended_open_ended" class="open-ended-child" data-state="assessing" data-child-type="openended">
|
||||
<div class="error">
|
||||
</div>
|
||||
<div class="prompt">
|
||||
Some prompt.
|
||||
</div>
|
||||
<textarea rows="30" cols="80" name="answer" class="answer short-form-response" id="input_open_ended" disabled="disabled">
|
||||
Test submission. Yaaaaaaay!
|
||||
</textarea>
|
||||
<div class="message-wrapper"></div>
|
||||
<div class="grader-status">
|
||||
<span class="grading" id="status_open_ended">Submitted for grading.</span>
|
||||
</div>
|
||||
<input type="button" value="Submit assessment" class="submit-button" name="show" style="display: none;">
|
||||
<input name="skip" class="skip-button" type="button" value="Skip Post-Assessment" style="display: none;">
|
||||
<div class="open-ended-action"></div>
|
||||
<span id="answer_open_ended"></span>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="oe-tools response-tools">
|
||||
<span class="oe-tools-label"></span>
|
||||
<input type="button" value="Reset" class="reset-button" name="reset" style="display: none;">
|
||||
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
|
||||
</div>
|
||||
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
|
||||
</div>
|
||||
|
||||
<a name="results">
|
||||
<div class="result-container">
|
||||
</div>
|
||||
</a></section><a name="results">
|
||||
|
||||
|
||||
</a></section><a name="results">
|
||||
|
||||
</a><div><a name="results">
|
||||
</a><a href="https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml">Edit</a> /
|
||||
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" onclick="javascript:getlog('i4x_MITx_6_002x_combinedopenended_CombinedOE', {
|
||||
<div class="result-container">
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
||||
<a name="results">
|
||||
</a>
|
||||
</section>
|
||||
<a name="results">
|
||||
</a>
|
||||
<div>
|
||||
<a name="results">
|
||||
</a>
|
||||
<a href="https://github.com/MITx/content-mit-6002x/tree/master/combinedopenended/CombinedOE.xml">
|
||||
Edit
|
||||
</a> /
|
||||
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" onclick="javascript:getlog('i4x_MITx_6_002x_combinedopenended_CombinedOE', {
|
||||
'location': 'i4x://MITx/6.002x/combinedopenended/CombinedOE',
|
||||
'xqa_key': 'KUBrWtK3RAaBALLbccHrXeD3RHOpmZ2A',
|
||||
'category': 'CombinedOpenEndedModule',
|
||||
'user': 'blah'
|
||||
})" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa_log">QA</a>
|
||||
</div>
|
||||
<div><a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_debug" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_trig">Staff Debug Info</a></div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#i4x_MITx_6_002x_combinedopenended_CombinedOE_debug" id="i4x_MITx_6_002x_combinedopenended_CombinedOE_trig">
|
||||
Staff Debug Info
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<section id="i4x_MITx_6_002x_combinedopenended_CombinedOE_xqa-modal" class="modal xqa-modal" style="width:80%; left:20%; height:80%; overflow:auto">
|
||||
<div class="inner-wrapper">
|
||||
|
||||
321
common/lib/xmodule/xmodule/js/fixtures/rubric.html
Normal file
321
common/lib/xmodule/xmodule/js/fixtures/rubric.html
Normal file
@@ -0,0 +1,321 @@
|
||||
<section id="combined-open-ended" class="combined-open-ended" data-location="i4x://test/2323/combinedopenended/b893eedec151441f8644187266ccce00" data-ajax-url="/courses/test/2323/Test2/modx/i4x://test/2323/combinedopenended/b893eedec151441f8644187266ccce00" data-allow_reset="False" data-state="initial" data-task-count="1" data-task-number="1" data-accept-file-upload="False">
|
||||
<div class="name">
|
||||
<h2>Open Response Assessment</h2>
|
||||
<div class="progress-container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="problemwrapper">
|
||||
<div class="status-bar">
|
||||
<table class="statustable">
|
||||
<tbody><tr>
|
||||
<td class="problemtype-container">
|
||||
<div class="problemtype">
|
||||
Open Response
|
||||
</div>
|
||||
</td>
|
||||
<td class="assessments-container">
|
||||
<div class="assessment-text">
|
||||
Assessments:
|
||||
</div>
|
||||
<div class="status-container">
|
||||
|
||||
<div class="status-elements">
|
||||
<section id="combined-open-ended-status" class="combined-open-ended-status">
|
||||
|
||||
<div class="statusitem statusitem-current" data-status-number="0">
|
||||
Peer
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<div class="item-container">
|
||||
<div class="visibility-control visibility-control-prompt">
|
||||
<div class="inner">
|
||||
</div>
|
||||
<a href="" class="question-header">Show Prompt</a>
|
||||
</div>
|
||||
<div class="problem-container">
|
||||
<div class="item">
|
||||
<section id="openended_open_ended" class="open-ended-child" data-state="post_assessment" data-child-type="openended">
|
||||
<div class="error"></div>
|
||||
<div class="prompt open" style="display: none;">
|
||||
<h3>Censorship in the Libraries</h3><p>'All of us can think of a book that we hope none of our children or any other children have taken off the shelf. But if I have the right to remove that book from the shelf -- that work I abhor -- then you also have exactly the same right and so does everyone else. And then we have no books left on the shelf for any of us.' --Katherine Paterson, Author
|
||||
</p><p>
|
||||
Write a persuasive essay to a newspaper reflecting your vies on censorship in libraries. Do you believe that certain materials, such as books, music, movies, magazines, etc., should be removed from the shelves if they are found offensive? Support your position with convincing arguments from your own experience, observations, and/or reading.
|
||||
</p>
|
||||
</div>
|
||||
<div class="visibility-control visibility-control-response">
|
||||
<div class="inner">
|
||||
</div>
|
||||
<span class="section-header section-header-response">Response</span>
|
||||
</div>
|
||||
<div class="answer short-form-response" id="input_open_ended"></div>
|
||||
|
||||
<div class="message-wrapper"></div>
|
||||
<div class="grader-status">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="file-upload"></div>
|
||||
|
||||
<input type="button" value="Submit post-assessment" class="submit-button" name="show" style="display: none;">
|
||||
<input name="skip" class="skip-button" type="button" value="Skip Post-Assessment" style="display: none;">
|
||||
|
||||
<div class="open-ended-action"></div>
|
||||
|
||||
<span id="answer_open_ended"></span>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe-tools response-tools">
|
||||
<span class="oe-tools-label"></span>
|
||||
<input type="button" value="Reset" class="reset-button" name="reset" style="display: inline-block;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="combined-rubric-container" data-status="shown" data-number="0" style="">
|
||||
<div class="visibility-control visibility-control-rubric">
|
||||
<div class="inner">
|
||||
</div>
|
||||
<span class="section-header section-header-rubric">Submitted Rubric</span>
|
||||
</div>
|
||||
<div class="rubric-header">
|
||||
<button class="rubric-collapse" href="#">Show Full Rubric</button>
|
||||
Scored rubric from grader 1
|
||||
</div>
|
||||
<div class="rubric">
|
||||
|
||||
<span class="rubric-category">
|
||||
Ideas
|
||||
</span> <br>
|
||||
<ul class="rubric-list">
|
||||
|
||||
<li class="rubric-list-item">
|
||||
<div class="rubric-label">
|
||||
<label class="choicegroup_incorrect wrapper-score-selection"></label>
|
||||
<span class="wrappable"> 0 points :
|
||||
Difficult for the reader to discern the main idea. Too brief or too repetitive to establish or maintain a focus.
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 1 points :
|
||||
Attempts a main idea. Sometimes loses focus or ineffectively displays focus.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 2 points :
|
||||
Presents a unifying theme or main idea, but may include minor tangents. Stays somewhat focused on topic and task.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 3 points :
|
||||
Presents a unifying theme or main idea without going off on tangents. Stays completely focused on topic and task.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span class="rubric-category">
|
||||
Content
|
||||
</span> <br>
|
||||
<ul class="rubric-list">
|
||||
|
||||
<li class="rubric-list-item">
|
||||
<div class="rubric-label">
|
||||
<label class="choicegroup_incorrect wrapper-score-selection"></label>
|
||||
<span class="wrappable"> 0 points :
|
||||
Includes little information with few or no details or unrelated details. Unsuccessful in attempts to explore any facets of the topic.
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 1 points :
|
||||
Includes little information and few or no details. Explores only one or two facets of the topic.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 2 points :
|
||||
Includes sufficient information and supporting details. (Details may not be fully developed; ideas may be listed.) Explores some facets of the topic.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 3 points :
|
||||
Includes in-depth information and exceptional supporting details that are fully developed. Explores all facets of the topic.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span class="rubric-category">
|
||||
Organization
|
||||
</span> <br>
|
||||
<ul class="rubric-list">
|
||||
|
||||
<li class="rubric-list-item">
|
||||
<div class="rubric-label">
|
||||
<label class="choicegroup_incorrect wrapper-score-selection"></label>
|
||||
<span class="wrappable"> 0 points :
|
||||
Ideas organized illogically, transitions weak, and response difficult to follow.
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 1 points :
|
||||
Attempts to logically organize ideas. Attempts to progress in an order that enhances meaning, and demonstrates use of transitions.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 2 points :
|
||||
Ideas organized logically. Progresses in an order that enhances meaning. Includes smooth transitions.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span class="rubric-category">
|
||||
Style
|
||||
</span> <br>
|
||||
<ul class="rubric-list">
|
||||
|
||||
<li class="rubric-list-item">
|
||||
<div class="rubric-label">
|
||||
<label class="choicegroup_incorrect wrapper-score-selection"></label>
|
||||
<span class="wrappable"> 0 points :
|
||||
Contains limited vocabulary, with many words used incorrectly. Demonstrates problems with sentence patterns.
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 1 points :
|
||||
Contains basic vocabulary, with words that are predictable and common. Contains mostly simple sentences (although there may be an attempt at more varied sentence patterns).
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 2 points :
|
||||
Includes vocabulary to make explanations detailed and precise. Includes varied sentence patterns, including complex sentences.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span class="rubric-category">
|
||||
Voice
|
||||
</span> <br>
|
||||
<ul class="rubric-list">
|
||||
|
||||
<li class="rubric-list-item">
|
||||
<div class="rubric-label">
|
||||
<label class="choicegroup_incorrect wrapper-score-selection"></label>
|
||||
<span class="wrappable"> 0 points :
|
||||
Demonstrates language and tone that may be inappropriate to task and reader.
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 1 points :
|
||||
Demonstrates an attempt to adjust language and tone to task and reader.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="rubric-list-item rubric-info-item" style="display: none;">
|
||||
<div class="rubric-label">
|
||||
<label class="rubric-elements-info">
|
||||
<span class="wrapper-score-selection"> </span>
|
||||
<span class="wrappable"> 2 points :
|
||||
Demonstrates effective adjustment of language and tone to task and reader.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="written-feedback">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="button" value="Next Step" class="next-step-button" name="reset" style="display: none;">
|
||||
|
||||
<section class="legend-container">
|
||||
</section>
|
||||
|
||||
<div class="result-container">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
0
common/lib/xmodule/xmodule/js/libpeerconnection.log
Normal file
0
common/lib/xmodule/xmodule/js/libpeerconnection.log
Normal file
@@ -1,3 +1,30 @@
|
||||
describe 'Rubric', ->
|
||||
beforeEach ->
|
||||
spyOn Logger, 'log'
|
||||
# load up some fixtures
|
||||
loadFixtures 'rubric.html'
|
||||
jasmine.Clock.useMock()
|
||||
@element = $('.combined-open-ended')
|
||||
@location = @element.data('location')
|
||||
|
||||
describe 'constructor', ->
|
||||
beforeEach ->
|
||||
@rub = new Rubric @element
|
||||
|
||||
it 'rubric should properly grab the element', ->
|
||||
expect(@rub.el).toEqual @element
|
||||
|
||||
describe 'initialize', ->
|
||||
beforeEach ->
|
||||
@rub = new Rubric @element
|
||||
@rub.initialize @location
|
||||
|
||||
it 'rubric correctly sets location', ->
|
||||
expect($(@rub.rubric_sel).data('location')).toEqual @location
|
||||
|
||||
it 'rubric correctly read', ->
|
||||
expect(@rub.categories.length).toEqual 5
|
||||
|
||||
describe 'CombinedOpenEnded', ->
|
||||
beforeEach ->
|
||||
spyOn Logger, 'log'
|
||||
@@ -13,7 +40,7 @@ describe 'CombinedOpenEnded', ->
|
||||
@combined = new CombinedOpenEnded @element
|
||||
|
||||
it 'set the element', ->
|
||||
expect(@combined.element).toEqual @element
|
||||
expect(@combined.el).toEqual @element
|
||||
|
||||
it 'get the correct values from data fields', ->
|
||||
expect(@combined.ajax_url).toEqual '/courses/MITx/6.002x/2012_Fall/modx/i4x://MITx/6.002x/combinedopenended/CombinedOE'
|
||||
@@ -77,7 +104,7 @@ describe 'CombinedOpenEnded', ->
|
||||
@combined.child_state = 'done'
|
||||
@combined.rebind()
|
||||
expect(@combined.answer_area.attr("disabled")).toBe("disabled")
|
||||
expect(@combined.next_problem).toHaveBeenCalled()
|
||||
expect(@combined.next_problem_button).toBe(":visible")
|
||||
|
||||
describe 'next_problem', ->
|
||||
beforeEach ->
|
||||
@@ -109,3 +136,5 @@ describe 'CombinedOpenEnded', ->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
class @Rubric
|
||||
constructor: () ->
|
||||
|
||||
@initialize: (location) ->
|
||||
$('.rubric').data("location", location)
|
||||
$('input[class="score-selection"]').change @tracking_callback
|
||||
rubric_category_sel: '.rubric-category'
|
||||
rubric_sel: '.rubric'
|
||||
|
||||
constructor: (el) ->
|
||||
@el = el
|
||||
|
||||
initialize: (location) =>
|
||||
@$(@rubric_sel).data("location", location)
|
||||
@$('input[class="score-selection"]').change @tracking_callback
|
||||
# set up the hotkeys
|
||||
$(window).unbind('keydown', @keypress_callback)
|
||||
$(window).keydown @keypress_callback
|
||||
# display the 'current' carat
|
||||
@categories = $('.rubric-category')
|
||||
@category = $(@categories.first())
|
||||
@category.prepend('> ')
|
||||
@categories = @$(@rubric_category_sel)
|
||||
@category = @$(@categories.first())
|
||||
@category_index = 0
|
||||
|
||||
|
||||
@keypress_callback: (event) =>
|
||||
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
keypress_callback: (event) =>
|
||||
# don't try to do this when user is typing in a text input
|
||||
if $(event.target).is('input, textarea')
|
||||
if @$(event.target).is('input, textarea')
|
||||
return
|
||||
# for when we select via top row
|
||||
if event.which >= 48 and event.which <= 57
|
||||
@@ -31,124 +38,158 @@ class @Rubric
|
||||
# if we actually have a current category (not past the end)
|
||||
if(@category_index <= @categories.length)
|
||||
# find the valid selections for this category
|
||||
inputs = $("input[name='score-selection-#{@category_index}']")
|
||||
inputs = @$("input[name='score-selection-#{@category_index}']")
|
||||
max_score = inputs.length - 1
|
||||
|
||||
if selected > max_score or selected < 0
|
||||
return
|
||||
inputs.filter("input[value=#{selected}]").click()
|
||||
|
||||
# move to the next category
|
||||
old_category_text = @category.html().substring(5)
|
||||
@category.html(old_category_text)
|
||||
@category_index++
|
||||
@category = $(@categories[@category_index])
|
||||
@category.prepend('> ')
|
||||
@category = @$(@categories[@category_index])
|
||||
|
||||
@tracking_callback: (event) ->
|
||||
target_selection = $(event.target).val()
|
||||
tracking_callback: (event) =>
|
||||
target_selection = @$(event.target).val()
|
||||
# chop off the beginning of the name so that we can get the number of the category
|
||||
category = $(event.target).data("category")
|
||||
location = $('.rubric').data('location')
|
||||
category = @$(event.target).data("category")
|
||||
location = @$(@rubric_sel).data('location')
|
||||
# probably want the original problem location as well
|
||||
|
||||
data = {location: location, selection: target_selection, category: category}
|
||||
Logger.log 'rubric_select', data
|
||||
|
||||
|
||||
# finds the scores for each rubric category
|
||||
@get_score_list: () =>
|
||||
get_score_list: () =>
|
||||
# find the number of categories:
|
||||
num_categories = $('.rubric-category').length
|
||||
num_categories = @$(@rubric_category_sel).length
|
||||
|
||||
score_lst = []
|
||||
# get the score for each one
|
||||
for i in [0..(num_categories-1)]
|
||||
score = $("input[name='score-selection-#{i}']:checked").val()
|
||||
score = @$("input[name='score-selection-#{i}']:checked").val()
|
||||
score_lst.push(score)
|
||||
|
||||
return score_lst
|
||||
|
||||
@get_total_score: () ->
|
||||
get_total_score: () =>
|
||||
score_lst = @get_score_list()
|
||||
tot = 0
|
||||
for score in score_lst
|
||||
tot += parseInt(score)
|
||||
return tot
|
||||
|
||||
@check_complete: () ->
|
||||
check_complete: () =>
|
||||
# check to see whether or not any categories have not been scored
|
||||
num_categories = $('.rubric-category').length
|
||||
num_categories = @$(@rubric_category_sel).length
|
||||
for i in [0..(num_categories-1)]
|
||||
score = $("input[name='score-selection-#{i}']:checked").val()
|
||||
score = @$("input[name='score-selection-#{i}']:checked").val()
|
||||
if score == undefined
|
||||
return false
|
||||
return true
|
||||
|
||||
class @CombinedOpenEnded
|
||||
constructor: (element) ->
|
||||
@element=element
|
||||
@reinitialize(element)
|
||||
|
||||
wrapper_sel: 'section.xmodule_CombinedOpenEndedModule'
|
||||
coe_sel: 'section.combined-open-ended'
|
||||
reset_button_sel: '.reset-button'
|
||||
next_step_sel: '.next-step-button'
|
||||
question_header_sel: '.question-header'
|
||||
submit_evaluation_sel: '.submit-evaluation-button'
|
||||
result_container_sel: 'div.result-container'
|
||||
combined_rubric_sel: '.combined-rubric-container'
|
||||
open_ended_child_sel: 'section.open-ended-child'
|
||||
error_sel: '.error'
|
||||
answer_area_sel: 'textarea.answer'
|
||||
answer_area_div_sel : 'div.answer'
|
||||
prompt_sel: '.prompt'
|
||||
rubric_wrapper_sel: '.rubric-wrapper'
|
||||
hint_wrapper_sel: '.hint-wrapper'
|
||||
message_wrapper_sel: '.message-wrapper'
|
||||
submit_button_sel: '.submit-button'
|
||||
skip_button_sel: '.skip-button'
|
||||
file_upload_sel: '.file-upload'
|
||||
file_upload_box_sel: '.file-upload-box'
|
||||
file_upload_preview_sel: '.file-upload-preview'
|
||||
fof_sel: 'textarea.feedback-on-feedback'
|
||||
sub_id_sel: 'input.submission_id'
|
||||
grader_id_sel: 'input.grader_id'
|
||||
grader_status_sel: '.grader-status'
|
||||
info_rubric_elements_sel: '.rubric-info-item'
|
||||
rubric_collapse_sel: '.rubric-collapse'
|
||||
next_rubric_sel: '.rubric-next-button'
|
||||
previous_rubric_sel: '.rubric-previous-button'
|
||||
oe_alert_sel: '.open-ended-alert'
|
||||
|
||||
constructor: (el) ->
|
||||
@el=el
|
||||
@$el = $(el)
|
||||
@reinitialize(el)
|
||||
$(window).keydown @keydown_handler
|
||||
$(window).keyup @keyup_handler
|
||||
|
||||
reinitialize: (element) ->
|
||||
@wrapper=$(element).find('section.xmodule_CombinedOpenEndedModule')
|
||||
@el = $(element).find('section.combined-open-ended')
|
||||
@combined_open_ended=$(element).find('section.combined-open-ended')
|
||||
@id = @el.data('id')
|
||||
@ajax_url = @el.data('ajax-url')
|
||||
@state = @el.data('state')
|
||||
@task_count = @el.data('task-count')
|
||||
@task_number = @el.data('task-number')
|
||||
@accept_file_upload = @el.data('accept-file-upload')
|
||||
@location = @el.data('location')
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
reinitialize: () ->
|
||||
@has_been_reset = false
|
||||
@wrapper=@$(@wrapper_sel)
|
||||
@coe = @$(@coe_sel)
|
||||
|
||||
@ajax_url = @coe.data('ajax-url')
|
||||
@get_html()
|
||||
@coe = @$(@coe_sel)
|
||||
|
||||
#Get data from combinedopenended
|
||||
@allow_reset = @coe.data('allow_reset')
|
||||
@id = @coe.data('id')
|
||||
@state = @coe.data('state')
|
||||
@task_count = @coe.data('task-count')
|
||||
@task_number = @coe.data('task-number')
|
||||
@accept_file_upload = @coe.data('accept-file-upload')
|
||||
@location = @coe.data('location')
|
||||
|
||||
# set up handlers for click tracking
|
||||
Rubric.initialize(@location)
|
||||
@rub = new Rubric(@coe)
|
||||
@rub.initialize(@location)
|
||||
@is_ctrl = false
|
||||
|
||||
@allow_reset = @el.data('allow_reset')
|
||||
@reset_button = @$('.reset-button')
|
||||
#Setup reset
|
||||
@reset_button = @$(@reset_button_sel)
|
||||
@reset_button.click @reset
|
||||
@next_problem_button = @$('.next-step-button')
|
||||
#Setup next problem
|
||||
@next_problem_button = @$(@next_step_sel)
|
||||
@next_problem_button.click @next_problem
|
||||
@status_container = @$('.status-elements')
|
||||
|
||||
@show_results_button=@$('.show-results-button')
|
||||
@show_results_button.click @show_results
|
||||
|
||||
@question_header = @$('.question-header')
|
||||
@question_header = @$(@question_header_sel)
|
||||
@question_header.click @collapse_question
|
||||
|
||||
# valid states: 'initial', 'assessing', 'post_assessment', 'done'
|
||||
Collapsible.setCollapsibles(@el)
|
||||
@submit_evaluation_button = $('.submit-evaluation-button')
|
||||
Collapsible.setCollapsibles(@$el)
|
||||
@submit_evaluation_button = @$(@submit_evaluation_sel)
|
||||
@submit_evaluation_button.click @message_post
|
||||
|
||||
@results_container = $('.result-container')
|
||||
@combined_rubric_container = $('.combined-rubric-container')
|
||||
|
||||
@legend_container= $('.legend-container')
|
||||
@show_legend_current()
|
||||
@results_container = @$(@result_container_sel)
|
||||
@combined_rubric_container = @$(@combined_rubric_sel)
|
||||
|
||||
# Where to put the rubric once we load it
|
||||
@el = $(element).find('section.open-ended-child')
|
||||
@errors_area = @$('.error')
|
||||
@answer_area = @$('textarea.answer')
|
||||
@prompt_container = @$('.prompt')
|
||||
@rubric_wrapper = @$('.rubric-wrapper')
|
||||
@hint_wrapper = @$('.hint-wrapper')
|
||||
@message_wrapper = @$('.message-wrapper')
|
||||
@submit_button = @$('.submit-button')
|
||||
@child_state = @el.data('state')
|
||||
@child_type = @el.data('child-type')
|
||||
@oe = @$(@open_ended_child_sel)
|
||||
|
||||
@errors_area = @$(@oe).find(@error_sel)
|
||||
@answer_area = @$(@oe).find(@answer_area_sel)
|
||||
@prompt_container = @$(@oe).find(@prompt_sel)
|
||||
@rubric_wrapper = @$(@oe).find(@rubric_wrapper_sel)
|
||||
@hint_wrapper = @$(@oe).find(@hint_wrapper_sel)
|
||||
@message_wrapper = @$(@oe).find(@message_wrapper_sel)
|
||||
@submit_button = @$(@oe).find(@submit_button_sel)
|
||||
@child_state = @oe.data('state')
|
||||
@child_type = @oe.data('child-type')
|
||||
if @child_type=="openended"
|
||||
@skip_button = @$('.skip-button')
|
||||
@skip_button = @$(@oe).find(@skip_button_sel)
|
||||
@skip_button.click @skip_post_assessment
|
||||
|
||||
@file_upload_area = @$('.file-upload')
|
||||
@file_upload_area = @$(@oe).find(@file_upload_sel)
|
||||
@can_upload_files = false
|
||||
@open_ended_child= @$('.open-ended-child')
|
||||
@open_ended_child= @$(@oe).find(@open_ended_child_sel)
|
||||
|
||||
@out_of_sync_message = 'The problem state got out of sync. Try reloading the page.'
|
||||
|
||||
@@ -162,71 +203,43 @@ class @CombinedOpenEnded
|
||||
|
||||
@rebind()
|
||||
|
||||
if @task_number>1
|
||||
@show_combined_rubric_current()
|
||||
@show_results_current()
|
||||
get_html_callback: (response) =>
|
||||
@coe.replaceWith(response.html)
|
||||
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
show_results_current: () =>
|
||||
data = {'task_number' : @task_number-1}
|
||||
$.postWithPrefix "#{@ajax_url}/get_results", data, (response) =>
|
||||
if response.success
|
||||
@results_container.after(response.html).remove()
|
||||
@results_container = $('div.result-container')
|
||||
@submit_evaluation_button = $('.submit-evaluation-button')
|
||||
@submit_evaluation_button.click @message_post
|
||||
Collapsible.setCollapsibles(@results_container)
|
||||
# make sure we still have click tracking
|
||||
$('.evaluation-response a').click @log_feedback_click
|
||||
$('input[name="evaluation-score"]').change @log_feedback_selection
|
||||
|
||||
show_results: (event) =>
|
||||
status_item = $(event.target).parent()
|
||||
status_number = status_item.data('status-number')
|
||||
data = {'task_number' : status_number}
|
||||
$.postWithPrefix "#{@ajax_url}/get_results", data, (response) =>
|
||||
if response.success
|
||||
@results_container.after(response.html).remove()
|
||||
@results_container = $('div.result-container')
|
||||
@submit_evaluation_button = $('.submit-evaluation-button')
|
||||
@submit_evaluation_button.click @message_post
|
||||
Collapsible.setCollapsibles(@results_container)
|
||||
else
|
||||
@gentle_alert response.error
|
||||
get_html: () =>
|
||||
url = "#{@ajax_url}/get_html"
|
||||
$.ajaxWithPrefix({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: {},
|
||||
success: @get_html_callback,
|
||||
async:false
|
||||
});
|
||||
|
||||
show_combined_rubric_current: () =>
|
||||
data = {}
|
||||
$.postWithPrefix "#{@ajax_url}/get_combined_rubric", data, (response) =>
|
||||
if response.success
|
||||
@combined_rubric_container.after(response.html).remove()
|
||||
@combined_rubric_container= $('div.combined_rubric_container')
|
||||
|
||||
show_status_current: () =>
|
||||
data = {}
|
||||
$.postWithPrefix "#{@ajax_url}/get_status", data, (response) =>
|
||||
if response.success
|
||||
@status_container.after(response.html).remove()
|
||||
@status_container= $('.status-elements')
|
||||
|
||||
show_legend_current: () =>
|
||||
data = {}
|
||||
$.postWithPrefix "#{@ajax_url}/get_legend", data, (response) =>
|
||||
if response.success
|
||||
@legend_container.after(response.html).remove()
|
||||
@legend_container= $('.legend-container')
|
||||
@combined_rubric_container= @$(@combined_rubric_sel)
|
||||
@toggle_rubric("")
|
||||
@rubric_collapse = @$(@rubric_collapse_sel)
|
||||
@rubric_collapse.click @toggle_rubric
|
||||
@hide_rubrics()
|
||||
@$(@previous_rubric_sel).click @previous_rubric
|
||||
@$(@next_rubric_sel).click @next_rubric
|
||||
if response.hide_reset
|
||||
@reset_button.hide()
|
||||
|
||||
message_post: (event)=>
|
||||
external_grader_message=$(event.target).parent().parent().parent()
|
||||
evaluation_scoring = $(event.target).parent()
|
||||
|
||||
fd = new FormData()
|
||||
feedback = evaluation_scoring.find('textarea.feedback-on-feedback')[0].value
|
||||
submission_id = external_grader_message.find('input.submission_id')[0].value
|
||||
grader_id = external_grader_message.find('input.grader_id')[0].value
|
||||
score = evaluation_scoring.find("input:radio[name='evaluation-score']:checked").val()
|
||||
feedback = @$(evaluation_scoring).find(@fof_sel)[0].value
|
||||
submission_id = @$(external_grader_message).find(@sub_id_sel)[0].value
|
||||
grader_id = @$(external_grader_message).find(@grader_id_sel)[0].value
|
||||
score = @$(evaluation_scoring).find("input:radio[name='evaluation-score']:checked").val()
|
||||
|
||||
fd.append('feedback', feedback)
|
||||
fd.append('submission_id', submission_id)
|
||||
@@ -244,7 +257,7 @@ class @CombinedOpenEnded
|
||||
contentType: false
|
||||
success: (response) =>
|
||||
@gentle_alert response.msg
|
||||
$('section.evaluation').slideToggle()
|
||||
@$('section.evaluation').slideToggle()
|
||||
@message_wrapper.html(response.message_html)
|
||||
|
||||
|
||||
@@ -256,11 +269,9 @@ class @CombinedOpenEnded
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.show()
|
||||
@reset_button.hide()
|
||||
@next_problem_button.hide()
|
||||
@hide_file_upload()
|
||||
@next_problem_button.hide()
|
||||
@hint_area.attr('disabled', false)
|
||||
if @task_number>1 or @child_state!='initial'
|
||||
@show_status_current()
|
||||
|
||||
if @task_number==1 and @child_state=='assessing'
|
||||
@prompt_hide()
|
||||
@@ -269,12 +280,14 @@ class @CombinedOpenEnded
|
||||
if @child_type=="openended"
|
||||
@skip_button.hide()
|
||||
if @allow_reset=="True"
|
||||
@show_results_current
|
||||
@show_combined_rubric_current()
|
||||
@reset_button.show()
|
||||
@submit_button.hide()
|
||||
@answer_area.attr("disabled", true)
|
||||
@replace_text_inputs()
|
||||
@hint_area.attr('disabled', true)
|
||||
if @task_number<@task_count
|
||||
@gentle_alert "Your score did not meet the criteria to move to the next step."
|
||||
else if @child_state == 'initial'
|
||||
@answer_area.attr("disabled", false)
|
||||
@submit_button.prop('value', 'Submit')
|
||||
@@ -286,12 +299,14 @@ class @CombinedOpenEnded
|
||||
@hide_file_upload()
|
||||
@submit_button.prop('value', 'Submit assessment')
|
||||
@submit_button.click @save_assessment
|
||||
@submit_button.attr("disabled",true)
|
||||
if @child_type == "openended"
|
||||
@submit_button.hide()
|
||||
@queueing()
|
||||
if @task_number==1 and @task_count==1
|
||||
@grader_status = $('.grader-status')
|
||||
@grader_status.html("<p>Response submitted for scoring.</p>")
|
||||
@grader_status = @$(@grader_status_sel)
|
||||
@grader_status.html("<span class='grading'>Your response has been submitted. Please check back later for your grade.</span> ")
|
||||
else if @child_type == "selfassessment"
|
||||
@setup_score_selection()
|
||||
else if @child_state == 'post_assessment'
|
||||
if @child_type=="openended"
|
||||
@skip_button.show()
|
||||
@@ -304,6 +319,7 @@ class @CombinedOpenEnded
|
||||
else
|
||||
@submit_button.click @message_post
|
||||
else if @child_state == 'done'
|
||||
@show_combined_rubric_current()
|
||||
@rubric_wrapper.hide()
|
||||
@answer_area.attr("disabled", true)
|
||||
@replace_text_inputs()
|
||||
@@ -312,11 +328,8 @@ class @CombinedOpenEnded
|
||||
if @child_type=="openended"
|
||||
@skip_button.hide()
|
||||
if @task_number<@task_count
|
||||
@next_problem()
|
||||
@next_problem_button.show()
|
||||
else
|
||||
if @task_number==1 and @task_count==1
|
||||
@show_combined_rubric_current()
|
||||
@show_results_current()
|
||||
@reset_button.show()
|
||||
|
||||
|
||||
@@ -326,14 +339,32 @@ class @CombinedOpenEnded
|
||||
find_hint_elements: ->
|
||||
@hint_area = @$('textarea.post_assessment')
|
||||
|
||||
replace_answer: (response) =>
|
||||
if response.success
|
||||
@rubric_wrapper.html(response.rubric_html)
|
||||
@rubric_wrapper.show()
|
||||
@rub = new Rubric(@coe)
|
||||
@rub.initialize(@location)
|
||||
@child_state = 'assessing'
|
||||
@find_assessment_elements()
|
||||
@rebind()
|
||||
answer_area_div = @$(@answer_area_div_sel)
|
||||
answer_area_div.html(response.student_response)
|
||||
else
|
||||
@can_upload_files = pre_can_upload_files
|
||||
@gentle_alert response.error
|
||||
|
||||
save_answer: (event) =>
|
||||
@submit_button.attr("disabled",true)
|
||||
@submit_button.hide()
|
||||
event.preventDefault()
|
||||
@answer_area.attr("disabled", true)
|
||||
max_filesize = 2*1000*1000 #2MB
|
||||
pre_can_upload_files = @can_upload_files
|
||||
if @child_state == 'initial'
|
||||
files = ""
|
||||
if @can_upload_files == true
|
||||
files = $('.file-upload-box')[0].files[0]
|
||||
files = @$(@file_upload_box_sel)[0].files[0]
|
||||
if files != undefined
|
||||
if files.size > max_filesize
|
||||
@can_upload_files = false
|
||||
@@ -351,21 +382,11 @@ class @CombinedOpenEnded
|
||||
data: fd
|
||||
processData: false
|
||||
contentType: false
|
||||
async: false
|
||||
success: (response) =>
|
||||
if response.success
|
||||
@rubric_wrapper.html(response.rubric_html)
|
||||
@rubric_wrapper.show()
|
||||
Rubric.initialize(@location)
|
||||
@answer_area.html(response.student_response)
|
||||
@child_state = 'assessing'
|
||||
@find_assessment_elements()
|
||||
@rebind()
|
||||
else
|
||||
@can_upload_files = pre_can_upload_files
|
||||
@gentle_alert response.error
|
||||
@replace_answer(response)
|
||||
|
||||
$.ajaxWithPrefix("#{@ajax_url}/save_answer",settings)
|
||||
|
||||
else
|
||||
@errors_area.html(@out_of_sync_message)
|
||||
|
||||
@@ -373,7 +394,7 @@ class @CombinedOpenEnded
|
||||
#Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
|
||||
if event.which == 17 && @is_ctrl==false
|
||||
@is_ctrl=true
|
||||
else if @is_ctrl==true && event.which == 13 && @child_state == 'assessing' && Rubric.check_complete()
|
||||
else if @is_ctrl==true && event.which == 13 && @child_state == 'assessing' && @rub.check_complete()
|
||||
@save_assessment(event)
|
||||
|
||||
keyup_handler: (event) =>
|
||||
@@ -382,10 +403,12 @@ class @CombinedOpenEnded
|
||||
@is_ctrl=false
|
||||
|
||||
save_assessment: (event) =>
|
||||
@submit_button.attr("disabled",true)
|
||||
@submit_button.hide()
|
||||
event.preventDefault()
|
||||
if @child_state == 'assessing' && Rubric.check_complete()
|
||||
checked_assessment = Rubric.get_total_score()
|
||||
score_list = Rubric.get_score_list()
|
||||
if @child_state == 'assessing' && @rub.check_complete()
|
||||
checked_assessment = @rub.get_total_score()
|
||||
score_list = @rub.get_score_list()
|
||||
data = {'assessment' : checked_assessment, 'score_list' : score_list}
|
||||
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
|
||||
if response.success
|
||||
@@ -440,9 +463,10 @@ class @CombinedOpenEnded
|
||||
@hint_wrapper.html('')
|
||||
@message_wrapper.html('')
|
||||
@child_state = 'initial'
|
||||
@combined_open_ended.after(response.html).remove()
|
||||
@coe.after(response.html).remove()
|
||||
@allow_reset="False"
|
||||
@reinitialize(@element)
|
||||
@has_been_reset = true
|
||||
@rebind()
|
||||
@reset_button.hide()
|
||||
else
|
||||
@@ -459,7 +483,7 @@ class @CombinedOpenEnded
|
||||
@hint_wrapper.html('')
|
||||
@message_wrapper.html('')
|
||||
@child_state = 'initial'
|
||||
@combined_open_ended.after(response.html).remove()
|
||||
@coe.after(response.html).remove()
|
||||
@reinitialize(@element)
|
||||
@rebind()
|
||||
@next_problem_button.hide()
|
||||
@@ -467,18 +491,18 @@ class @CombinedOpenEnded
|
||||
@gentle_alert "Moved to next step."
|
||||
else
|
||||
@gentle_alert "Your score did not meet the criteria to move to the next step."
|
||||
@show_results_current()
|
||||
@show_combined_rubric_current()
|
||||
else
|
||||
@errors_area.html(response.error)
|
||||
else
|
||||
@errors_area.html(@out_of_sync_message)
|
||||
|
||||
gentle_alert: (msg) =>
|
||||
if @el.find('.open-ended-alert').length
|
||||
@el.find('.open-ended-alert').remove()
|
||||
if @$el.find(@oe_alert_sel).length
|
||||
@$el.find(@oe_alert_sel).remove()
|
||||
alert_elem = "<div class='open-ended-alert'>" + msg + "</div>"
|
||||
@el.find('.open-ended-action').after(alert_elem)
|
||||
@el.find('.open-ended-alert').css(opacity: 0).animate(opacity: 1, 700)
|
||||
@$el.find('.open-ended-action').after(alert_elem)
|
||||
@$el.find(@oe_alert_sel).css(opacity: 0).animate(opacity: 1, 700)
|
||||
|
||||
queueing: =>
|
||||
if @child_state=="assessing" and @child_type=="openended"
|
||||
@@ -500,8 +524,8 @@ class @CombinedOpenEnded
|
||||
@can_upload_files = true
|
||||
@file_upload_area.html('<input type="file" class="file-upload-box"><img class="file-upload-preview" src="#" alt="Uploaded image" />')
|
||||
@file_upload_area.show()
|
||||
$('.file-upload-preview').hide()
|
||||
$('.file-upload-box').change @preview_image
|
||||
@$(@file_upload_preview_sel).hide()
|
||||
@$(@file_upload_box_sel).change @preview_image
|
||||
else
|
||||
@gentle_alert 'File uploads are required for this question, but are not supported in this browser. Try the newest version of google chrome. Alternatively, if you have uploaded the image to the web, you can paste a link to it into the answer box.'
|
||||
|
||||
@@ -519,33 +543,66 @@ class @CombinedOpenEnded
|
||||
|
||||
# wrap this so that it can be mocked
|
||||
reload: ->
|
||||
location.reload()
|
||||
@reinitialize()
|
||||
|
||||
collapse_question: () =>
|
||||
collapse_question: (event) =>
|
||||
@prompt_container.slideToggle()
|
||||
@prompt_container.toggleClass('open')
|
||||
if @question_header.text() == "(Hide)"
|
||||
new_text = "(Show)"
|
||||
if @question_header.text() == "Hide Prompt"
|
||||
new_text = "Show Prompt"
|
||||
Logger.log 'oe_hide_question', {location: @location}
|
||||
else
|
||||
Logger.log 'oe_show_question', {location: @location}
|
||||
new_text = "(Hide)"
|
||||
new_text = "Hide Prompt"
|
||||
@question_header.text(new_text)
|
||||
return false
|
||||
|
||||
hide_rubrics: () =>
|
||||
rubrics = @$(@combined_rubric_sel)
|
||||
for rub in rubrics
|
||||
if @$(rub).data('status')=="shown"
|
||||
@$(rub).show()
|
||||
else
|
||||
@$(rub).hide()
|
||||
|
||||
next_rubric: =>
|
||||
@shift_rubric(1)
|
||||
return false
|
||||
|
||||
previous_rubric: =>
|
||||
@shift_rubric(-1)
|
||||
return false
|
||||
|
||||
shift_rubric: (i) =>
|
||||
rubrics = @$(@combined_rubric_sel)
|
||||
number = 0
|
||||
for rub in rubrics
|
||||
if @$(rub).data('status')=="shown"
|
||||
number = @$(rub).data('number')
|
||||
@$(rub).data('status','hidden')
|
||||
if i==1 and number < rubrics.length - 1
|
||||
number = number + i
|
||||
|
||||
if i==-1 and number>0
|
||||
number = number + i
|
||||
|
||||
@$(rubrics[number]).data('status', 'shown')
|
||||
@hide_rubrics()
|
||||
|
||||
prompt_show: () =>
|
||||
if @prompt_container.is(":hidden")==true
|
||||
@prompt_container.slideToggle()
|
||||
@prompt_container.toggleClass('open')
|
||||
@question_header.text("(Hide)")
|
||||
@question_header.text("Hide Prompt")
|
||||
|
||||
prompt_hide: () =>
|
||||
if @prompt_container.is(":visible")==true
|
||||
@prompt_container.slideToggle()
|
||||
@prompt_container.toggleClass('open')
|
||||
@question_header.text("(Show)")
|
||||
@question_header.text("Show Prompt")
|
||||
|
||||
log_feedback_click: (event) ->
|
||||
link_text = $(event.target).html()
|
||||
link_text = @$(event.target).html()
|
||||
if link_text == 'See full feedback'
|
||||
Logger.log 'oe_show_full_feedback', {}
|
||||
else if link_text == 'Respond to Feedback'
|
||||
@@ -553,32 +610,44 @@ class @CombinedOpenEnded
|
||||
else
|
||||
generated_event_type = link_text.toLowerCase().replace(" ","_")
|
||||
Logger.log "oe_" + generated_event_type, {}
|
||||
|
||||
log_feedback_selection: (event) ->
|
||||
target_selection = $(event.target).val()
|
||||
target_selection = @$(event.target).val()
|
||||
Logger.log 'oe_feedback_response_selected', {value: target_selection}
|
||||
|
||||
remove_attribute: (name) =>
|
||||
if $('.file-upload-preview').attr(name)
|
||||
$('.file-upload-preview')[0].removeAttribute(name)
|
||||
if @$(@file_upload_preview_sel).attr(name)
|
||||
@$(@file_upload_preview_sel)[0].removeAttribute(name)
|
||||
|
||||
preview_image: () =>
|
||||
if $('.file-upload-box')[0].files && $('.file-upload-box')[0].files[0]
|
||||
if @$(@file_upload_box_sel)[0].files && @$(@file_upload_box_sel)[0].files[0]
|
||||
reader = new FileReader()
|
||||
reader.onload = (e) =>
|
||||
max_dim = 150
|
||||
@remove_attribute('src')
|
||||
@remove_attribute('height')
|
||||
@remove_attribute('width')
|
||||
$('.file-upload-preview').attr('src', e.target.result)
|
||||
height_px = $('.file-upload-preview')[0].height
|
||||
width_px = $('.file-upload-preview')[0].width
|
||||
@$(@file_upload_preview_sel).attr('src', e.target.result)
|
||||
height_px = @$(@file_upload_preview_sel)[0].height
|
||||
width_px = @$(@file_upload_preview_sel)[0].width
|
||||
scale_factor = 0
|
||||
if height_px>width_px
|
||||
scale_factor = height_px/max_dim
|
||||
else
|
||||
scale_factor = width_px/max_dim
|
||||
$('.file-upload-preview')[0].width = width_px/scale_factor
|
||||
$('.file-upload-preview')[0].height = height_px/scale_factor
|
||||
$('.file-upload-preview').show()
|
||||
reader.readAsDataURL($('.file-upload-box')[0].files[0])
|
||||
@$(@file_upload_preview_sel)[0].width = width_px/scale_factor
|
||||
@$(@file_upload_preview_sel)[0].height = height_px/scale_factor
|
||||
@$(@file_upload_preview_sel).show()
|
||||
reader.readAsDataURL(@$(@file_upload_box_sel)[0].files[0])
|
||||
|
||||
toggle_rubric: (event) =>
|
||||
info_rubric_elements = @$(@info_rubric_elements_sel)
|
||||
info_rubric_elements.slideToggle()
|
||||
return false
|
||||
|
||||
setup_score_selection: () =>
|
||||
@$("input[class='score-selection']").change @graded_callback
|
||||
|
||||
graded_callback: () =>
|
||||
if @rub.check_complete()
|
||||
@submit_button.attr("disabled",false)
|
||||
@submit_button.show()
|
||||
|
||||
@@ -3,10 +3,20 @@
|
||||
# Can (and should be) expanded upon when our problem list
|
||||
# becomes more sophisticated
|
||||
class @PeerGrading
|
||||
|
||||
peer_grading_sel: '.peer-grading'
|
||||
peer_grading_container_sel: '.peer-grading-container'
|
||||
error_container_sel: '.error-container'
|
||||
message_container_sel: '.message-container'
|
||||
problem_button_sel: '.problem-button'
|
||||
problem_list_sel: '.problem-list'
|
||||
progress_bar_sel: '.progress-bar'
|
||||
|
||||
constructor: (element) ->
|
||||
@peer_grading_container = $('.peer-grading')
|
||||
@el = element
|
||||
@peer_grading_container = @$(@peer_grading_sel)
|
||||
@use_single_location = @peer_grading_container.data('use-single-location')
|
||||
@peer_grading_outer_container = $('.peer-grading-container')
|
||||
@peer_grading_outer_container = @$(@peer_grading_container_sel)
|
||||
@ajax_url = @peer_grading_container.data('ajax-url')
|
||||
|
||||
if @use_single_location.toLowerCase() == "true"
|
||||
@@ -14,23 +24,27 @@ class @PeerGrading
|
||||
@activate_problem()
|
||||
else
|
||||
#Otherwise, activate the panel view.
|
||||
@error_container = $('.error-container')
|
||||
@error_container = @$(@error_container_sel)
|
||||
@error_container.toggle(not @error_container.is(':empty'))
|
||||
|
||||
@message_container = $('.message-container')
|
||||
@message_container = @$(@message_container_sel)
|
||||
@message_container.toggle(not @message_container.is(':empty'))
|
||||
|
||||
@problem_button = $('.problem-button')
|
||||
@problem_button = @$(@problem_button_sel)
|
||||
@problem_button.click @show_results
|
||||
|
||||
@problem_list = $('.problem-list')
|
||||
@problem_list = @$(@problem_list_sel)
|
||||
@construct_progress_bar()
|
||||
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
construct_progress_bar: () =>
|
||||
problems = @problem_list.find('tr').next()
|
||||
problems.each( (index, element) =>
|
||||
problem = $(element)
|
||||
progress_bar = problem.find('.progress-bar')
|
||||
progress_bar = problem.find(@progress_bar_sel)
|
||||
bar_value = parseInt(problem.data('graded'))
|
||||
bar_max = parseInt(problem.data('required')) + bar_value
|
||||
progress_bar.progressbar({value: bar_value, max: bar_max})
|
||||
@@ -43,10 +57,10 @@ class @PeerGrading
|
||||
if response.success
|
||||
@peer_grading_outer_container.after(response.html).remove()
|
||||
backend = new PeerGradingProblemBackend(@ajax_url, false)
|
||||
new PeerGradingProblem(backend)
|
||||
new PeerGradingProblem(backend, @el)
|
||||
else
|
||||
@gentle_alert response.error
|
||||
|
||||
activate_problem: () =>
|
||||
backend = new PeerGradingProblemBackend(@ajax_url, false)
|
||||
new PeerGradingProblem(backend)
|
||||
new PeerGradingProblem(backend, @el)
|
||||
@@ -158,11 +158,47 @@ class @PeerGradingProblemBackend
|
||||
return response
|
||||
|
||||
class @PeerGradingProblem
|
||||
constructor: (backend) ->
|
||||
@prompt_wrapper = $('.prompt-wrapper')
|
||||
|
||||
prompt_wrapper_sel: '.prompt-wrapper'
|
||||
peer_grading_container_sel: '.peer-grading-container'
|
||||
submission_container_sel: '.submission-container'
|
||||
prompt_container_sel: '.prompt-container'
|
||||
rubric_container_sel: '.rubric-container'
|
||||
flag_student_container_sel: '.flag-student-container'
|
||||
answer_unknown_container_sel: '.answer-unknown-container'
|
||||
calibration_panel_sel: '.calibration-panel'
|
||||
grading_panel_sel: '.grading-panel'
|
||||
content_panel_sel: '.content-panel'
|
||||
grading_message_sel: '.grading-message'
|
||||
question_header_sel: '.question-header'
|
||||
flag_submission_confirmation_sel: '.flag-submission-confirmation'
|
||||
flag_submission_confirmation_button_sel: '.flag-submission-confirmation-button'
|
||||
flag_submission_removal_button_sel: '.flag-submission-removal-button'
|
||||
grading_wrapper_sel: '.grading-wrapper'
|
||||
calibration_feedback_sel: '.calibration-feedback'
|
||||
interstitial_page_sel: '.interstitial-page'
|
||||
calibration_interstitial_page_sel: '.calibration-interstitial-page'
|
||||
error_container_sel: '.error-container'
|
||||
feedback_area_sel: '.feedback-area'
|
||||
score_selection_container_sel: '.score-selection-container'
|
||||
rubric_selection_container_sel: '.rubric-selection-container'
|
||||
submit_button_sel: '.submit-button'
|
||||
action_button_sel: '.action-button'
|
||||
calibration_feedback_button_sel: '.calibration-feedback-button'
|
||||
interstitial_page_button_sel: '.interstitial-page-button'
|
||||
calibration_interstitial_page_button_sel: '.calibration-interstitial-page-button'
|
||||
flag_checkbox_sel: '.flag-checkbox'
|
||||
answer_unknown_checkbox_sel: '.answer-unknown-checkbox'
|
||||
calibration_text_sel: '.calibration-text'
|
||||
grading_text_sel: '.grading-text'
|
||||
calibration_feedback_wrapper_sel: '.calibration-feedback-wrapper'
|
||||
|
||||
constructor: (backend, el) ->
|
||||
@el = el
|
||||
@prompt_wrapper = $(@prompt_wrapper_sel)
|
||||
@backend = backend
|
||||
@is_ctrl = false
|
||||
|
||||
@el = $(@peer_grading_container_sel)
|
||||
|
||||
# get the location of the problem
|
||||
@location = $('.peer-grading').data('location')
|
||||
@@ -172,57 +208,55 @@ class @PeerGradingProblem
|
||||
return
|
||||
|
||||
# get the other elements we want to fill in
|
||||
@submission_container = $('.submission-container')
|
||||
@prompt_container = $('.prompt-container')
|
||||
@rubric_container = $('.rubric-container')
|
||||
@flag_student_container = $('.flag-student-container')
|
||||
@answer_unknown_container = $('.answer-unknown-container')
|
||||
@calibration_panel = $('.calibration-panel')
|
||||
@grading_panel = $('.grading-panel')
|
||||
@content_panel = $('.content-panel')
|
||||
@grading_message = $('.grading-message')
|
||||
@submission_container = @$(@submission_container_sel)
|
||||
@prompt_container = @$(@prompt_container_sel)
|
||||
@rubric_container = @$(@rubric_container_sel)
|
||||
@flag_student_container = @$(@flag_student_container_sel)
|
||||
@answer_unknown_container = @$(@answer_unknown_container_sel)
|
||||
@calibration_panel = @$(@calibration_panel_sel)
|
||||
@grading_panel = @$(@grading_panel_sel)
|
||||
@content_panel = @$(@content_panel_sel)
|
||||
@grading_message = @$(@grading_message_sel)
|
||||
@grading_message.hide()
|
||||
@question_header = $('.question-header')
|
||||
@question_header = @$(@question_header_sel)
|
||||
@question_header.click @collapse_question
|
||||
@flag_submission_confirmation = $('.flag-submission-confirmation')
|
||||
@flag_submission_confirmation_button = $('.flag-submission-confirmation-button')
|
||||
@flag_submission_removal_button = $('.flag-submission-removal-button')
|
||||
@flag_submission_confirmation = @$(@flag_submission_confirmation_sel)
|
||||
@flag_submission_confirmation_button = @$(@flag_submission_confirmation_button_sel)
|
||||
@flag_submission_removal_button = @$(@flag_submission_removal_button_sel)
|
||||
|
||||
@flag_submission_confirmation_button.click @close_dialog_box
|
||||
@flag_submission_removal_button.click @remove_flag
|
||||
|
||||
@grading_wrapper =$('.grading-wrapper')
|
||||
@calibration_feedback_panel = $('.calibration-feedback')
|
||||
@interstitial_page = $('.interstitial-page')
|
||||
@grading_wrapper = @$(@grading_wrapper_sel)
|
||||
@calibration_feedback_panel = @$(@calibration_feedback_sel)
|
||||
@interstitial_page = @$(@interstitial_page_sel)
|
||||
@interstitial_page.hide()
|
||||
|
||||
@calibration_interstitial_page = $('.calibration-interstitial-page')
|
||||
@calibration_interstitial_page = @$(@calibration_interstitial_page_sel)
|
||||
@calibration_interstitial_page.hide()
|
||||
|
||||
@error_container = $('.error-container')
|
||||
@error_container = @$(@error_container_sel)
|
||||
|
||||
@submission_key_input = $("input[name='submission-key']")
|
||||
@essay_id_input = $("input[name='essay-id']")
|
||||
@feedback_area = $('.feedback-area')
|
||||
@essay_id_input = @$("input[name='essay-id']")
|
||||
@feedback_area = @$(@feedback_area_sel)
|
||||
|
||||
@score_selection_container = $('.score-selection-container')
|
||||
@rubric_selection_container = $('.rubric-selection-container')
|
||||
@score_selection_container = @$(@score_selection_container_sel)
|
||||
@rubric_selection_container = @$(@rubric_selection_container_sel)
|
||||
@grade = null
|
||||
@calibration = null
|
||||
|
||||
@submit_button = $('.submit-button')
|
||||
@action_button = $('.action-button')
|
||||
@calibration_feedback_button = $('.calibration-feedback-button')
|
||||
@interstitial_page_button = $('.interstitial-page-button')
|
||||
@calibration_interstitial_page_button = $('.calibration-interstitial-page-button')
|
||||
@flag_student_checkbox = $('.flag-checkbox')
|
||||
@answer_unknown_checkbox = $('.answer-unknown-checkbox')
|
||||
@submit_button = @$(@submit_button_sel)
|
||||
@action_button = @$(@action_button_sel)
|
||||
@calibration_feedback_button = @$(@calibration_feedback_button_sel)
|
||||
@interstitial_page_button = @$(@interstitial_page_button_sel)
|
||||
@calibration_interstitial_page_button = @$(@calibration_interstitial_page_button_sel)
|
||||
@flag_student_checkbox = @$(@flag_checkbox_sel)
|
||||
@answer_unknown_checkbox = @$(@answer_unknown_checkbox_sel)
|
||||
|
||||
$(window).keydown @keydown_handler
|
||||
$(window).keyup @keyup_handler
|
||||
|
||||
@collapse_question()
|
||||
|
||||
Collapsible.setCollapsibles(@content_panel)
|
||||
|
||||
# Set up the click event handlers
|
||||
@@ -230,7 +264,7 @@ class @PeerGradingProblem
|
||||
@calibration_feedback_button.click =>
|
||||
@calibration_feedback_panel.hide()
|
||||
@grading_wrapper.show()
|
||||
@gentle_alert "Calibration essay saved. Fetched the next essay."
|
||||
@gentle_alert "Calibration essay saved. Fetching the next essay."
|
||||
@is_calibrated_check()
|
||||
|
||||
@interstitial_page_button.click =>
|
||||
@@ -251,6 +285,10 @@ class @PeerGradingProblem
|
||||
|
||||
@is_calibrated_check()
|
||||
|
||||
# locally scoped jquery.
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
|
||||
##########
|
||||
#
|
||||
@@ -269,8 +307,8 @@ class @PeerGradingProblem
|
||||
|
||||
construct_data: () ->
|
||||
data =
|
||||
rubric_scores: Rubric.get_score_list()
|
||||
score: Rubric.get_total_score()
|
||||
rubric_scores: @rub.get_score_list()
|
||||
score: @rub.get_total_score()
|
||||
location: @location
|
||||
submission_id: @essay_id_input.val()
|
||||
submission_key: @submission_key_input.val()
|
||||
@@ -282,10 +320,12 @@ class @PeerGradingProblem
|
||||
|
||||
submit_calibration_essay: ()=>
|
||||
data = @construct_data()
|
||||
@submit_button.hide()
|
||||
@backend.post('save_calibration_essay', data, @calibration_callback)
|
||||
|
||||
submit_grade: () =>
|
||||
data = @construct_data()
|
||||
@submit_button.hide()
|
||||
@backend.post('save_grade', data, @submission_callback)
|
||||
|
||||
|
||||
@@ -298,13 +338,15 @@ class @PeerGradingProblem
|
||||
remove_flag: () =>
|
||||
@flag_student_checkbox.removeAttr("checked")
|
||||
@close_dialog_box()
|
||||
@submit_button.attr('disabled', true)
|
||||
|
||||
close_dialog_box: () =>
|
||||
$( ".flag-submission-confirmation" ).dialog('close')
|
||||
$(@flag_submission_confirmation_sel).dialog('close')
|
||||
|
||||
flag_box_checked: () =>
|
||||
if @flag_student_checkbox.is(':checked')
|
||||
$( ".flag-submission-confirmation" ).dialog({ height: 400, width: 400 })
|
||||
@$(@flag_submission_confirmation_sel).dialog({ height: 400, width: 400 })
|
||||
@submit_button.attr('disabled', false)
|
||||
|
||||
# called after we perform an is_student_calibrated check
|
||||
calibration_check_callback: (response) =>
|
||||
@@ -344,7 +386,11 @@ class @PeerGradingProblem
|
||||
if response.success
|
||||
@is_calibrated_check()
|
||||
@grading_message.fadeIn()
|
||||
@grading_message.html("<p>Successfully saved your feedback. Fetched the next essay.</p>")
|
||||
message = "<p>Successfully saved your feedback. Fetching the next essay."
|
||||
if response.required_done
|
||||
message = message + " You have completed the required number of gradings."
|
||||
message = message + "</p>"
|
||||
@grading_message.html(message)
|
||||
else
|
||||
if response.error
|
||||
@render_error(response.error)
|
||||
@@ -353,12 +399,16 @@ class @PeerGradingProblem
|
||||
|
||||
# called after a grade is selected on the interface
|
||||
graded_callback: (event) =>
|
||||
ev = @$(event.target).parent().parent()
|
||||
ul = ev.parent().parent()
|
||||
ul.find(".rubric-label-selected").removeClass('rubric-label-selected')
|
||||
ev.addClass('rubric-label-selected')
|
||||
# check to see whether or not any categories have not been scored
|
||||
if Rubric.check_complete()
|
||||
if @rub.check_complete()
|
||||
# show button if we have scores for all categories
|
||||
@grading_message.hide()
|
||||
@show_submit_button()
|
||||
@grade = Rubric.get_total_score()
|
||||
@grade = @rub.get_total_score()
|
||||
|
||||
keydown_handler: (event) =>
|
||||
#Previously, responses were submitted when hitting enter. Add in a modifier that ensures that ctrl+enter is needed.
|
||||
@@ -395,18 +445,20 @@ class @PeerGradingProblem
|
||||
# Display the right text
|
||||
# both versions of the text are written into the template itself
|
||||
# we only need to show/hide the correct ones at the correct time
|
||||
@calibration_panel.find('.calibration-text').show()
|
||||
@grading_panel.find('.calibration-text').show()
|
||||
@calibration_panel.find('.grading-text').hide()
|
||||
@grading_panel.find('.grading-text').hide()
|
||||
@calibration_panel.find(@calibration_text_sel).show()
|
||||
@grading_panel.find(@calibration_text_sel).show()
|
||||
@calibration_panel.find(@grading_text_sel).hide()
|
||||
@grading_panel.find(@grading_text_sel).hide()
|
||||
@flag_student_container.hide()
|
||||
@answer_unknown_container.hide()
|
||||
|
||||
@feedback_area.val("")
|
||||
|
||||
@submit_button.show()
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_calibration_essay
|
||||
|
||||
@submit_button.attr('disabled', true)
|
||||
@scroll_to_top()
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@@ -425,16 +477,20 @@ class @PeerGradingProblem
|
||||
# Display the correct text
|
||||
# both versions of the text are written into the template itself
|
||||
# we only need to show/hide the correct ones at the correct time
|
||||
@calibration_panel.find('.calibration-text').hide()
|
||||
@grading_panel.find('.calibration-text').hide()
|
||||
@calibration_panel.find('.grading-text').show()
|
||||
@grading_panel.find('.grading-text').show()
|
||||
@calibration_panel.find(@calibration_text_sel).hide()
|
||||
@grading_panel.find(@calibration_text_sel).hide()
|
||||
@calibration_panel.find(@grading_text_sel).show()
|
||||
@grading_panel.find(@grading_text_sel).show()
|
||||
@flag_student_container.show()
|
||||
@answer_unknown_container.show()
|
||||
@feedback_area.val("")
|
||||
|
||||
@flag_student_checkbox.removeAttr("checked")
|
||||
@submit_button.show()
|
||||
@submit_button.unbind('click')
|
||||
@submit_button.click @submit_grade
|
||||
@submit_button.attr('disabled', true)
|
||||
@scroll_to_top()
|
||||
else if response.error
|
||||
@render_error(response.error)
|
||||
else
|
||||
@@ -463,13 +519,14 @@ class @PeerGradingProblem
|
||||
@submit_button.hide()
|
||||
@action_button.hide()
|
||||
@calibration_feedback_panel.hide()
|
||||
Rubric.initialize(@location)
|
||||
@rub = new Rubric(@el)
|
||||
@rub.initialize(@location)
|
||||
|
||||
|
||||
render_calibration_feedback: (response) =>
|
||||
# display correct grade
|
||||
@calibration_feedback_panel.slideDown()
|
||||
calibration_wrapper = $('.calibration-feedback-wrapper')
|
||||
calibration_wrapper = @$(@calibration_feedback_wrapper_sel)
|
||||
calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>")
|
||||
|
||||
score = parseInt(@grade)
|
||||
@@ -482,11 +539,11 @@ class @PeerGradingProblem
|
||||
|
||||
if response.actual_rubric != undefined
|
||||
calibration_wrapper.append("<div>Instructor Scored Rubric: #{response.actual_rubric}</div>")
|
||||
if response.actual_feedback!=undefined
|
||||
if response.actual_feedback.feedback!=undefined
|
||||
calibration_wrapper.append("<div>Instructor Feedback: #{response.actual_feedback}</div>")
|
||||
|
||||
# disable score selection and submission from the grading interface
|
||||
$("input[name='score-selection']").attr('disabled', true)
|
||||
@$("input[name='score-selection']").attr('disabled', true)
|
||||
@submit_button.hide()
|
||||
@calibration_feedback_button.show()
|
||||
|
||||
@@ -508,23 +565,30 @@ class @PeerGradingProblem
|
||||
@action_button.show()
|
||||
|
||||
show_submit_button: () =>
|
||||
@submit_button.attr('disabled', false)
|
||||
@submit_button.show()
|
||||
|
||||
setup_score_selection: (max_score) =>
|
||||
# And now hook up an event handler again
|
||||
$("input[class='score-selection']").change @graded_callback
|
||||
@$("input[class='score-selection']").change @graded_callback
|
||||
|
||||
gentle_alert: (msg) =>
|
||||
@grading_message.fadeIn()
|
||||
@grading_message.html("<p>" + msg + "</p>")
|
||||
|
||||
collapse_question: () =>
|
||||
collapse_question: (event) =>
|
||||
@prompt_container.slideToggle()
|
||||
@prompt_container.toggleClass('open')
|
||||
if @question_header.text() == "(Hide)"
|
||||
Logger.log 'peer_grading_hide_question', {location: @location}
|
||||
new_text = "(Show)"
|
||||
if @question_header.text() == "Hide Prompt"
|
||||
new_text = "Show Prompt"
|
||||
Logger.log 'oe_hide_question', {location: @location}
|
||||
else
|
||||
Logger.log 'peer_grading_show_question', {location: @location}
|
||||
new_text = "(Hide)"
|
||||
Logger.log 'oe_show_question', {location: @location}
|
||||
new_text = "Hide Prompt"
|
||||
@question_header.text(new_text)
|
||||
return false
|
||||
|
||||
scroll_to_top: () =>
|
||||
$('html, body').animate({
|
||||
scrollTop: $(".peer-grading").offset().top
|
||||
}, 200)
|
||||
|
||||
@@ -55,6 +55,7 @@ def modulestore(name='default'):
|
||||
|
||||
return _MODULESTORES[name]
|
||||
|
||||
|
||||
_loc_singleton = None
|
||||
def loc_mapper():
|
||||
"""
|
||||
@@ -69,3 +70,42 @@ def loc_mapper():
|
||||
_loc_singleton = LocMapperStore(settings.modulestore_options)
|
||||
return _loc_singleton
|
||||
|
||||
|
||||
def clear_existing_modulestores():
|
||||
"""
|
||||
Clear the existing modulestore instances, causing
|
||||
them to be re-created when accessed again.
|
||||
|
||||
This is useful for flushing state between unit tests.
|
||||
"""
|
||||
_MODULESTORES.clear()
|
||||
|
||||
|
||||
def editable_modulestore(name='default'):
|
||||
"""
|
||||
Retrieve a modulestore that we can modify.
|
||||
This is useful for tests that need to insert test
|
||||
data into the modulestore.
|
||||
|
||||
Currently, only Mongo-backed modulestores can be modified.
|
||||
Returns `None` if no editable modulestore is available.
|
||||
"""
|
||||
|
||||
# Try to retrieve the ModuleStore
|
||||
# Depending on the settings, this may or may not
|
||||
# be editable.
|
||||
store = modulestore(name)
|
||||
|
||||
# If this is a `MixedModuleStore`, then we will need
|
||||
# to retrieve the actual Mongo instance.
|
||||
# We assume that the default is Mongo.
|
||||
if hasattr(store, 'modulestores'):
|
||||
store = store.modulestores['default']
|
||||
|
||||
# At this point, we either have the ability to create
|
||||
# items in the store, or we do not.
|
||||
if hasattr(store, 'create_xmodule'):
|
||||
return store
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -9,7 +9,8 @@ INHERITABLE_METADATA = (
|
||||
# intended to be set per-course, but can be overridden in for specific
|
||||
# elements. Can be a float.
|
||||
'days_early_for_beta',
|
||||
'giturl' # for git edit link
|
||||
'giturl', # for git edit link
|
||||
'static_asset_path', # for static assets placed outside xcontent contentstore
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,46 @@
|
||||
"""
|
||||
Modulestore configuration for test cases.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
from django.test import TestCase
|
||||
from xmodule.modulestore.django import editable_modulestore, \
|
||||
clear_existing_modulestores
|
||||
|
||||
from django.conf import settings
|
||||
import xmodule.modulestore.django
|
||||
from unittest.util import safe_repr
|
||||
|
||||
def mixed_store_config(data_dir, mappings):
|
||||
"""
|
||||
Return a `MixedModuleStore` configuration, which provides
|
||||
access to both Mongo- and XML-backed courses.
|
||||
|
||||
`data_dir` is the directory from which to load XML-backed courses.
|
||||
`mappings` is a dictionary mapping course IDs to modulestores, for example:
|
||||
|
||||
{
|
||||
'MITx/2.01x/2013_Spring': 'xml',
|
||||
'edx/999/2013_Spring': 'default'
|
||||
}
|
||||
|
||||
where 'xml' and 'default' are the two options provided by this configuration,
|
||||
mapping (respectively) to XML-backed and Mongo-backed modulestores..
|
||||
"""
|
||||
mongo_config = mongo_store_config(data_dir)
|
||||
xml_config = xml_store_config(data_dir)
|
||||
|
||||
store = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
|
||||
'OPTIONS': {
|
||||
'mappings': mappings,
|
||||
'stores': {
|
||||
'default': mongo_config['default'],
|
||||
'xml': xml_config['default']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
|
||||
def mongo_store_config(data_dir):
|
||||
@@ -27,6 +62,7 @@ def mongo_store_config(data_dir):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
@@ -45,23 +81,22 @@ def draft_mongo_store_config(data_dir):
|
||||
'render_template': 'mitxmako.shortcuts.render_to_string'
|
||||
}
|
||||
|
||||
return {
|
||||
store = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
'direct': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': modulestore_options
|
||||
}
|
||||
}
|
||||
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
|
||||
def xml_store_config(data_dir):
|
||||
"""
|
||||
Defines default module store using XMLModuleStore.
|
||||
"""
|
||||
return {
|
||||
store = {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
@@ -71,12 +106,48 @@ def xml_store_config(data_dir):
|
||||
}
|
||||
}
|
||||
|
||||
store['direct'] = store['default']
|
||||
return store
|
||||
|
||||
|
||||
class ModuleStoreTestCase(TestCase):
|
||||
""" Subclass for any test case that uses the mongodb
|
||||
module store. This populates a uniquely named modulestore
|
||||
collection with templates before running the TestCase
|
||||
and drops it they are finished. """
|
||||
"""
|
||||
Subclass for any test case that uses a ModuleStore.
|
||||
Ensures that the ModuleStore is cleaned before/after each test.
|
||||
|
||||
Usage:
|
||||
|
||||
1. Create a subclass of `ModuleStoreTestCase`
|
||||
2. Use Django's @override_settings decorator to use
|
||||
the desired modulestore configuration.
|
||||
|
||||
For example:
|
||||
|
||||
MIXED_CONFIG = mixed_store_config(data_dir, mappings)
|
||||
|
||||
@override_settings(MODULESTORE=MIXED_CONFIG)
|
||||
class FooTest(ModuleStoreTestCase):
|
||||
# ...
|
||||
|
||||
3. Use factories (e.g. `CourseFactory`, `ItemFactory`) to populate
|
||||
the modulestore with test data.
|
||||
|
||||
NOTE:
|
||||
* For Mongo-backed courses (created with `CourseFactory`),
|
||||
the state of the course will be reset before/after each
|
||||
test method executes.
|
||||
|
||||
* For XML-backed courses, the course state will NOT
|
||||
reset between test methods (although it will reset
|
||||
between test classes)
|
||||
|
||||
The reason is: XML courses are not editable, so to reset
|
||||
a course you have to reload it from disk, which is slow.
|
||||
|
||||
If you do need to reset an XML course, use
|
||||
`clear_existing_modulestores()` directly in
|
||||
your `setUp()` method.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def update_course(course, data):
|
||||
@@ -89,107 +160,68 @@ class ModuleStoreTestCase(TestCase):
|
||||
|
||||
'data' is a dictionary with an entry for each CourseField we want to update.
|
||||
"""
|
||||
store = xmodule.modulestore.django.modulestore()
|
||||
store = editable_modulestore('direct')
|
||||
store.update_metadata(course.location, data)
|
||||
updated_course = store.get_instance(course.id, course.location)
|
||||
return updated_course
|
||||
|
||||
@staticmethod
|
||||
def flush_mongo_except_templates():
|
||||
def drop_mongo_collection():
|
||||
"""
|
||||
Delete everything in the module store except templates.
|
||||
If using a Mongo-backed modulestore, drop the collection.
|
||||
"""
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
|
||||
# This query means: every item in the collection
|
||||
# that is not a template
|
||||
query = {"_id.course": {"$ne": "templates"}}
|
||||
# This will return the mongo-backed modulestore
|
||||
# even if we're using a mixed modulestore
|
||||
store = editable_modulestore()
|
||||
|
||||
# Remove everything except templates
|
||||
modulestore.collection.remove(query)
|
||||
modulestore.collection.drop()
|
||||
if hasattr(store, 'collection'):
|
||||
store.collection.drop()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Flush the mongo store and set up templates.
|
||||
Delete the existing modulestores, causing them to be reloaded.
|
||||
"""
|
||||
|
||||
# Use a uuid to differentiate
|
||||
# the mongo collections on jenkins.
|
||||
cls.orig_modulestore = copy.deepcopy(settings.MODULESTORE)
|
||||
if 'direct' not in settings.MODULESTORE:
|
||||
settings.MODULESTORE['direct'] = settings.MODULESTORE['default']
|
||||
|
||||
settings.MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
settings.MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % uuid4().hex
|
||||
xmodule.modulestore.django._MODULESTORES.clear()
|
||||
|
||||
print settings.MODULESTORE
|
||||
|
||||
# Clear out any existing modulestores,
|
||||
# which will cause them to be re-created
|
||||
# the next time they are accessed.
|
||||
clear_existing_modulestores()
|
||||
TestCase.setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""
|
||||
Revert to the old modulestore settings.
|
||||
Drop the existing modulestores, causing them to be reloaded.
|
||||
Clean up any data stored in Mongo.
|
||||
"""
|
||||
# Clean up by flushing the Mongo modulestore
|
||||
cls.drop_mongo_collection()
|
||||
|
||||
# Clean up by dropping the collection
|
||||
modulestore = xmodule.modulestore.django.modulestore()
|
||||
modulestore.collection.drop()
|
||||
# Clear out the existing modulestores,
|
||||
# which will cause them to be re-created
|
||||
# the next time they are accessed.
|
||||
# We do this at *both* setup and teardown just to be safe.
|
||||
clear_existing_modulestores()
|
||||
|
||||
xmodule.modulestore.django._MODULESTORES.clear()
|
||||
|
||||
# Restore the original modulestore settings
|
||||
settings.MODULESTORE = cls.orig_modulestore
|
||||
TestCase.tearDownClass()
|
||||
|
||||
def _pre_setup(self):
|
||||
"""
|
||||
Remove everything but the templates before each test.
|
||||
Flush the ModuleStore before each test.
|
||||
"""
|
||||
|
||||
# Flush anything that is not a template
|
||||
ModuleStoreTestCase.flush_mongo_except_templates()
|
||||
# Flush the Mongo modulestore
|
||||
ModuleStoreTestCase.drop_mongo_collection()
|
||||
|
||||
# Call superclass implementation
|
||||
super(ModuleStoreTestCase, self)._pre_setup()
|
||||
|
||||
def _post_teardown(self):
|
||||
"""
|
||||
Flush everything we created except the templates.
|
||||
Flush the ModuleStore after each test.
|
||||
"""
|
||||
# Flush anything that is not a template
|
||||
ModuleStoreTestCase.flush_mongo_except_templates()
|
||||
ModuleStoreTestCase.drop_mongo_collection()
|
||||
|
||||
# Call superclass implementation
|
||||
super(ModuleStoreTestCase, self)._post_teardown()
|
||||
|
||||
|
||||
def assert2XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a success status (between 200 and 299)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a success status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 200 and status_code < 300, msg=msg)
|
||||
|
||||
def assert3XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a redirection status (between 300 and 399)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a redirection status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 300 and status_code < 400, msg=msg)
|
||||
|
||||
def assert4XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a client error status (between 400 and 499)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a client error status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 400 and status_code < 500, msg=msg)
|
||||
|
||||
def assert5XX(self, status_code, msg=None):
|
||||
"""
|
||||
Assert that the given value is a server error status (between 500 and 599)
|
||||
"""
|
||||
msg = self._formatMessage(msg, "%s is not a server error status" % safe_repr(status_code))
|
||||
self.assertTrue(status_code >= 500 and status_code < 600, msg=msg)
|
||||
|
||||
@@ -5,11 +5,12 @@ from uuid import uuid4
|
||||
from pytz import UTC
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.django import editable_modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xblock.core import Scope
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
|
||||
|
||||
class XModuleCourseFactory(Factory):
|
||||
"""
|
||||
Factory for XModule courses.
|
||||
@@ -25,10 +26,7 @@ class XModuleCourseFactory(Factory):
|
||||
display_name = kwargs.pop('display_name', None)
|
||||
location = Location('i4x', org, number, 'course', Location.clean(display_name))
|
||||
|
||||
try:
|
||||
store = modulestore('direct')
|
||||
except KeyError:
|
||||
store = modulestore()
|
||||
store = editable_modulestore('direct')
|
||||
|
||||
# Write the data to the mongo datastore
|
||||
new_course = store.create_xmodule(location)
|
||||
@@ -117,7 +115,7 @@ class XModuleItemFactory(Factory):
|
||||
if not isinstance(data, basestring):
|
||||
data.update(template.get('data'))
|
||||
|
||||
store = modulestore('direct')
|
||||
store = editable_modulestore('direct')
|
||||
|
||||
# This code was based off that in cms/djangoapps/contentstore/views.py
|
||||
parent = store.get_item(parent_location)
|
||||
|
||||
@@ -10,7 +10,9 @@ from xblock.runtime import KeyValueStore, InvalidScopeError
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.draft import DraftModuleStore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
|
||||
from xmodule.contentstore.mongo import MongoContentStore
|
||||
|
||||
from xmodule.modulestore.tests.test_modulestore import check_path_to_location
|
||||
|
||||
@@ -35,7 +37,7 @@ class TestMongoModuleStore(object):
|
||||
# is ok only as long as none of the tests modify the db.
|
||||
# If (when!) that changes, need to either reload the db, or load
|
||||
# once and copy over to a tmp db for each test.
|
||||
cls.store = cls.initdb()
|
||||
cls.store, cls.content_store, cls.draft_store = cls.initdb()
|
||||
|
||||
@classmethod
|
||||
def teardownClass(cls):
|
||||
@@ -46,10 +48,28 @@ class TestMongoModuleStore(object):
|
||||
def initdb():
|
||||
# connect to the db
|
||||
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS)
|
||||
# since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class
|
||||
# as well
|
||||
content_store = MongoContentStore(HOST, DB)
|
||||
#
|
||||
# Also test draft store imports
|
||||
#
|
||||
draft_store = DraftModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS)
|
||||
# Explicitly list the courses to load (don't want the big one)
|
||||
courses = ['toy', 'simple']
|
||||
import_from_xml(store, DATA_DIR, courses)
|
||||
return store
|
||||
courses = ['toy', 'simple', 'simple_with_draft']
|
||||
import_from_xml(store, DATA_DIR, courses, draft_store=draft_store, static_content_store=content_store)
|
||||
|
||||
# also test a course with no importing of static content
|
||||
import_from_xml(
|
||||
store,
|
||||
DATA_DIR,
|
||||
['test_import_course'],
|
||||
static_content_store=content_store,
|
||||
do_import_static=False,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
return store, content_store, draft_store
|
||||
|
||||
@staticmethod
|
||||
def destroy_db(connection):
|
||||
@@ -77,10 +97,12 @@ class TestMongoModuleStore(object):
|
||||
def test_get_courses(self):
|
||||
'''Make sure the course objects loaded properly'''
|
||||
courses = self.store.get_courses()
|
||||
assert_equals(len(courses), 2)
|
||||
assert_equals(len(courses), 4)
|
||||
courses.sort(key=lambda c: c.id)
|
||||
assert_equals(courses[0].id, 'edX/simple/2012_Fall')
|
||||
assert_equals(courses[1].id, 'edX/toy/2012_Fall')
|
||||
assert_equals(courses[1].id, 'edX/simple_with_draft/2012_Fall')
|
||||
assert_equals(courses[2].id, 'edX/test_import_course/2012_Fall')
|
||||
assert_equals(courses[3].id, 'edX/toy/2012_Fall')
|
||||
|
||||
def test_loads(self):
|
||||
assert_not_equals(
|
||||
@@ -112,6 +134,13 @@ class TestMongoModuleStore(object):
|
||||
'''Make sure that path_to_location works'''
|
||||
check_path_to_location(self.store)
|
||||
|
||||
def test_xlinter(self):
|
||||
'''
|
||||
Run through the xlinter, we know the 'toy' course has violations, but the
|
||||
number will continue to grow over time, so just check > 0
|
||||
'''
|
||||
assert_not_equals(perform_xlint(DATA_DIR, ['toy']), 0)
|
||||
|
||||
def test_get_courses_has_no_templates(self):
|
||||
courses = self.store.get_courses()
|
||||
for course in courses:
|
||||
@@ -129,7 +158,7 @@ class TestMongoModuleStore(object):
|
||||
|
||||
Assumes the information is desired for courses[1] ('toy' course).
|
||||
"""
|
||||
return courses[1].tabs[index]['name']
|
||||
return courses[2].tabs[index]['name']
|
||||
|
||||
# There was a bug where model.save was not getting called after the static tab name
|
||||
# was set set for tabs that have a URL slug. 'Syllabus' and 'Resources' fall into that
|
||||
|
||||
@@ -51,7 +51,10 @@ def import_static_content(modules, course_loc, course_data_path, static_content_
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
#then commit the content
|
||||
static_content_store.save(content)
|
||||
try:
|
||||
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
|
||||
remap_dict[fullname_with_subpath] = content_loc.name
|
||||
@@ -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,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
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,
|
||||
using org and course as the location org and course.
|
||||
@@ -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
|
||||
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(
|
||||
@@ -116,8 +124,17 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
course_data_path = path(data_dir) / module.data_dir
|
||||
course_location = module.location
|
||||
|
||||
log.debug('======> IMPORTING course to location {0}'.format(course_location))
|
||||
|
||||
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
|
||||
# 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 -
|
||||
@@ -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
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# first pass to find everything in /static/
|
||||
import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store,
|
||||
_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
|
||||
for module in xml_module_store.modules[course_id].itervalues():
|
||||
if module.category == 'course':
|
||||
@@ -156,7 +190,8 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
log.debug('importing module location {0}'.format(module.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
|
||||
if draft_store is not 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,
|
||||
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()))
|
||||
|
||||
@@ -196,7 +232,7 @@ def import_module(module, store, course_data_path, static_content_store,
|
||||
else:
|
||||
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
|
||||
# portable strings (e.g. /static/)
|
||||
module_data = rewrite_nonportable_content_links(
|
||||
@@ -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
|
||||
# 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)))
|
||||
|
||||
|
||||
@@ -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
|
||||
# they would have been filtered out from the non-draft store export
|
||||
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']
|
||||
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,
|
||||
seq_location = seq_location._replace(org=target_location_namespace.org,
|
||||
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:
|
||||
sequential.children.insert(index, module.location.url())
|
||||
if non_draft_location.url() not in sequential.children:
|
||||
sequential.children.insert(index, non_draft_location.url())
|
||||
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,
|
||||
source_location_namespace, target_location_namespace, allow_not_found=True)
|
||||
for child in module.get_children():
|
||||
|
||||
@@ -7,7 +7,9 @@ from xmodule.progress import Progress
|
||||
from xmodule.stringify import stringify_children
|
||||
import self_assessment_module
|
||||
import open_ended_module
|
||||
from functools import partial
|
||||
from .combined_open_ended_rubric import CombinedOpenEndedRubric, GRADER_TYPE_IMAGE_DICT, HUMAN_GRADER_TYPE, LEGEND_LIST
|
||||
from peer_grading_service import PeerGradingService, MockPeerGradingService, GradingServiceError
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -31,8 +33,17 @@ ACCEPT_FILE_UPLOAD = False
|
||||
TRUE_DICT = ["True", True, "TRUE", "true"]
|
||||
|
||||
HUMAN_TASK_TYPE = {
|
||||
'selfassessment': "Self Assessment",
|
||||
'openended': "edX Assessment",
|
||||
'selfassessment': "Self",
|
||||
'openended': "edX",
|
||||
'ml_grading.conf' : "AI",
|
||||
'peer_grading.conf' : "Peer",
|
||||
}
|
||||
|
||||
HUMAN_STATES = {
|
||||
'intitial': "Not started.",
|
||||
'assessing': "Being scored.",
|
||||
'intermediate_done': "Scoring finished.",
|
||||
'done': "Complete.",
|
||||
}
|
||||
|
||||
# Default value that controls whether or not to skip basic spelling checks in the controller
|
||||
@@ -56,7 +67,6 @@ class CombinedOpenEndedV1Module():
|
||||
ajax actions implemented by combined open ended module are:
|
||||
'reset' -- resets the whole combined open ended module and returns to the first child moduleresource_string
|
||||
'next_problem' -- moves to the next child module
|
||||
'get_results' -- gets results from a given child module
|
||||
|
||||
Types of children. Task is synonymous with child module, so each combined open ended module
|
||||
incorporates multiple children (tasks):
|
||||
@@ -106,6 +116,11 @@ class CombinedOpenEndedV1Module():
|
||||
self.accept_file_upload = instance_state.get('accept_file_upload', ACCEPT_FILE_UPLOAD) in TRUE_DICT
|
||||
self.skip_basic_checks = instance_state.get('skip_spelling_checks', SKIP_BASIC_CHECKS) in TRUE_DICT
|
||||
|
||||
if system.open_ended_grading_interface:
|
||||
self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
|
||||
else:
|
||||
self.peer_gs = MockPeerGradingService()
|
||||
|
||||
self.required_peer_grading = instance_state.get('required_peer_grading', 3)
|
||||
self.peer_grader_count = instance_state.get('peer_grader_count', 3)
|
||||
self.min_to_calibrate = instance_state.get('min_to_calibrate', 3)
|
||||
@@ -300,6 +315,7 @@ class CombinedOpenEndedV1Module():
|
||||
'accept_file_upload': self.accept_file_upload,
|
||||
'location': self.location,
|
||||
'legend_list': LEGEND_LIST,
|
||||
'human_state': HUMAN_STATES.get(self.state,"Not started.")
|
||||
}
|
||||
|
||||
return context
|
||||
@@ -334,6 +350,14 @@ class CombinedOpenEndedV1Module():
|
||||
self.update_task_states()
|
||||
return self.current_task.get_html(self.system)
|
||||
|
||||
def get_html_ajax(self, data):
|
||||
"""
|
||||
Get HTML in AJAX callback
|
||||
data - Needed to preserve AJAX structure
|
||||
Output: Dictionary with html attribute
|
||||
"""
|
||||
return {'html': self.get_html()}
|
||||
|
||||
def get_current_attributes(self, task_number):
|
||||
"""
|
||||
Gets the min and max score to attempt attributes of the specified task.
|
||||
@@ -419,6 +443,7 @@ class CombinedOpenEndedV1Module():
|
||||
grader_type = grader_types[0]
|
||||
else:
|
||||
grader_type = "IN"
|
||||
grader_types = ["IN"]
|
||||
|
||||
if grader_type in HUMAN_GRADER_TYPE:
|
||||
human_grader_name = HUMAN_GRADER_TYPE[grader_type]
|
||||
@@ -446,9 +471,22 @@ class CombinedOpenEndedV1Module():
|
||||
'feedback_dicts': feedback_dicts,
|
||||
'grader_ids': grader_ids,
|
||||
'submission_ids': submission_ids,
|
||||
'success' : True
|
||||
}
|
||||
return last_response_dict
|
||||
|
||||
def extract_human_name_from_task(self, task_xml):
|
||||
tree = etree.fromstring(task_xml)
|
||||
payload = tree.xpath("/openended/openendedparam/grader_payload")
|
||||
if len(payload)==0:
|
||||
task_name = "selfassessment"
|
||||
else:
|
||||
inner_payload = json.loads(payload[0].text)
|
||||
task_name = inner_payload['grader_settings']
|
||||
|
||||
human_task = HUMAN_TASK_TYPE[task_name]
|
||||
return human_task
|
||||
|
||||
def update_task_states(self):
|
||||
"""
|
||||
Updates the task state of the combined open ended module with the task state of the current child module.
|
||||
@@ -481,6 +519,51 @@ class CombinedOpenEndedV1Module():
|
||||
pass
|
||||
return return_html
|
||||
|
||||
def check_if_student_has_done_needed_grading(self):
|
||||
"""
|
||||
Checks with the ORA server to see if the student has completed the needed peer grading to be shown their grade.
|
||||
For example, if a student submits one response, and three peers grade their response, the student
|
||||
cannot see their grades and feedback unless they reciprocate.
|
||||
Output:
|
||||
success - boolean indicator of success
|
||||
allowed_to_submit - boolean indicator of whether student has done their needed grading or not
|
||||
error_message - If not success, explains why
|
||||
"""
|
||||
student_id = self.system.anonymous_student_id
|
||||
success = False
|
||||
allowed_to_submit = True
|
||||
try:
|
||||
response = self.peer_gs.get_data_for_location(self.location.url(), student_id)
|
||||
count_graded = response['count_graded']
|
||||
count_required = response['count_required']
|
||||
student_sub_count = response['student_sub_count']
|
||||
count_available = response['count_available']
|
||||
success = True
|
||||
except GradingServiceError:
|
||||
# This is a dev_facing_error
|
||||
log.error("Could not contact external open ended graders for location {0} and student {1}".format(
|
||||
self.location, 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
|
||||
except KeyError:
|
||||
log.error("Invalid response from grading server for location {0} and student {1}".format(self.location, student_id))
|
||||
error_message = "Received invalid response from the graders. Please notify course staff."
|
||||
return success, allowed_to_submit, error_message
|
||||
if count_graded >= count_required or count_available==0:
|
||||
error_message = ""
|
||||
return success, allowed_to_submit, error_message
|
||||
else:
|
||||
allowed_to_submit = False
|
||||
# This is a student_facing_error
|
||||
error_string = ("<h4>Feedback not available yet</h4>"
|
||||
"<p>You need to peer grade {0} more submissions in order to see your feedback.</p>"
|
||||
"<p>You have graded responses from {1} students, and {2} students have graded your submissions. </p>"
|
||||
"<p>You have made {3} submissions.</p>")
|
||||
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_rubric(self, _data):
|
||||
"""
|
||||
Gets the results of a given grader via ajax.
|
||||
@@ -488,30 +571,39 @@ class CombinedOpenEndedV1Module():
|
||||
Output: Dictionary to be rendered via ajax that contains the result html.
|
||||
"""
|
||||
all_responses = []
|
||||
loop_up_to_task = self.current_task_number + 1
|
||||
for i in xrange(0, loop_up_to_task):
|
||||
all_responses.append(self.get_last_response(i))
|
||||
rubric_scores = [all_responses[i]['rubric_scores'] for i in xrange(0, len(all_responses)) if
|
||||
len(all_responses[i]['rubric_scores']) > 0 and all_responses[i]['grader_types'][
|
||||
0] in HUMAN_GRADER_TYPE.keys()]
|
||||
grader_types = [all_responses[i]['grader_types'] for i in xrange(0, len(all_responses)) if
|
||||
len(all_responses[i]['grader_types']) > 0 and all_responses[i]['grader_types'][
|
||||
0] in HUMAN_GRADER_TYPE.keys()]
|
||||
feedback_items = [all_responses[i]['feedback_items'] for i in xrange(0, len(all_responses)) if
|
||||
len(all_responses[i]['feedback_items']) > 0 and all_responses[i]['grader_types'][
|
||||
0] in HUMAN_GRADER_TYPE.keys()]
|
||||
rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']),
|
||||
rubric_scores,
|
||||
grader_types, feedback_items)
|
||||
success, can_see_rubric, error = self.check_if_student_has_done_needed_grading()
|
||||
if not can_see_rubric:
|
||||
return {'html' : self.system.render_template('{0}/combined_open_ended_hidden_results.html'.format(self.TEMPLATE_DIR), {'error' : error}), 'success' : True, 'hide_reset' : True}
|
||||
|
||||
contexts = []
|
||||
rubric_number = self.current_task_number
|
||||
if self.ready_to_reset:
|
||||
rubric_number+=1
|
||||
response = self.get_last_response(rubric_number)
|
||||
score_length = len(response['grader_types'])
|
||||
for z in xrange(score_length):
|
||||
if response['grader_types'][z] in HUMAN_GRADER_TYPE:
|
||||
try:
|
||||
feedback = response['feedback_dicts'][z].get('feedback', '')
|
||||
except TypeError:
|
||||
return {'success' : False}
|
||||
rubric_scores = [[response['rubric_scores'][z]]]
|
||||
grader_types = [[response['grader_types'][z]]]
|
||||
feedback_items = [[response['feedback_items'][z]]]
|
||||
rubric_html = self.rubric_renderer.render_combined_rubric(stringify_children(self.static_data['rubric']),
|
||||
rubric_scores,
|
||||
grader_types, feedback_items)
|
||||
contexts.append({
|
||||
'result': rubric_html,
|
||||
'task_name': 'Scored rubric',
|
||||
'feedback' : feedback
|
||||
})
|
||||
|
||||
response_dict = all_responses[-1]
|
||||
context = {
|
||||
'results': rubric_html,
|
||||
'task_name': 'Scored Rubric',
|
||||
'class_name': 'combined-rubric-container'
|
||||
'results': contexts,
|
||||
}
|
||||
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
return {'html': html, 'success': True, 'hide_reset' : False}
|
||||
|
||||
def get_legend(self, _data):
|
||||
"""
|
||||
@@ -525,59 +617,6 @@ class CombinedOpenEndedV1Module():
|
||||
html = self.system.render_template('{0}/combined_open_ended_legend.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_results(self, _data):
|
||||
"""
|
||||
Gets the results of a given grader via ajax.
|
||||
Input: AJAX data dictionary
|
||||
Output: Dictionary to be rendered via ajax that contains the result html.
|
||||
"""
|
||||
self.update_task_states()
|
||||
loop_up_to_task = self.current_task_number + 1
|
||||
all_responses = []
|
||||
for i in xrange(0, loop_up_to_task):
|
||||
all_responses.append(self.get_last_response(i))
|
||||
context_list = []
|
||||
for ri in all_responses:
|
||||
for i in xrange(0, len(ri['rubric_scores'])):
|
||||
feedback = ri['feedback_dicts'][i].get('feedback', '')
|
||||
rubric_data = self.rubric_renderer.render_rubric(stringify_children(self.static_data['rubric']),
|
||||
ri['rubric_scores'][i])
|
||||
if rubric_data['success']:
|
||||
rubric_html = rubric_data['html']
|
||||
else:
|
||||
rubric_html = ''
|
||||
context = {
|
||||
'rubric_html': rubric_html,
|
||||
'grader_type': ri['grader_type'],
|
||||
'feedback': feedback,
|
||||
'grader_id': ri['grader_ids'][i],
|
||||
'submission_id': ri['submission_ids'][i],
|
||||
}
|
||||
context_list.append(context)
|
||||
feedback_table = self.system.render_template('{0}/open_ended_result_table.html'.format(self.TEMPLATE_DIR), {
|
||||
'context_list': context_list,
|
||||
'grader_type_image_dict': GRADER_TYPE_IMAGE_DICT,
|
||||
'human_grader_types': HUMAN_GRADER_TYPE,
|
||||
'rows': 50,
|
||||
'cols': 50,
|
||||
})
|
||||
context = {
|
||||
'results': feedback_table,
|
||||
'task_name': "Feedback",
|
||||
'class_name': "result-container",
|
||||
}
|
||||
html = self.system.render_template('{0}/combined_open_ended_results.html'.format(self.TEMPLATE_DIR), context)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def get_status_ajax(self, _data):
|
||||
"""
|
||||
Gets the results of a given grader via ajax.
|
||||
Input: AJAX data dictionary
|
||||
Output: Dictionary to be rendered via ajax that contains the result html.
|
||||
"""
|
||||
html = self.get_status(True)
|
||||
return {'html': html, 'success': True}
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
"""
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
@@ -592,10 +631,11 @@ class CombinedOpenEndedV1Module():
|
||||
handlers = {
|
||||
'next_problem': self.next_problem,
|
||||
'reset': self.reset,
|
||||
'get_results': self.get_results,
|
||||
'get_combined_rubric': self.get_rubric,
|
||||
'get_status': self.get_status_ajax,
|
||||
'get_legend': self.get_legend,
|
||||
'get_last_response': self.get_last_response_ajax,
|
||||
'get_current_state': self.get_current_state,
|
||||
'get_html': self.get_html_ajax,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -605,6 +645,17 @@ class CombinedOpenEndedV1Module():
|
||||
d = handlers[dispatch](data)
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def get_current_state(self, data):
|
||||
return self.get_context()
|
||||
|
||||
def get_last_response_ajax(self, data):
|
||||
"""
|
||||
Get the last response via ajax callback
|
||||
data - Needed to preserve ajax callback structure
|
||||
Output: Last response dictionary
|
||||
"""
|
||||
return self.get_last_response(self.current_task_number)
|
||||
|
||||
def next_problem(self, _data):
|
||||
"""
|
||||
Called via ajax to advance to the next problem.
|
||||
@@ -623,10 +674,12 @@ class CombinedOpenEndedV1Module():
|
||||
if self.state != self.DONE:
|
||||
if not self.ready_to_reset:
|
||||
return self.out_of_sync_error(data)
|
||||
|
||||
if self.student_attempts >= self.max_attempts-1:
|
||||
if self.student_attempts==self.max_attempts-1:
|
||||
self.student_attempts +=1
|
||||
success, can_reset, error = self.check_if_student_has_done_needed_grading()
|
||||
if not can_reset:
|
||||
return {'error': error, 'success': False}
|
||||
if self.student_attempts >= self.max_attempts - 1:
|
||||
if self.student_attempts == self.max_attempts - 1:
|
||||
self.student_attempts += 1
|
||||
return {
|
||||
'success': False,
|
||||
# This is a student_facing_error
|
||||
@@ -638,7 +691,7 @@ class CombinedOpenEndedV1Module():
|
||||
self.student_attempts +=1
|
||||
self.state = self.INITIAL
|
||||
self.ready_to_reset = False
|
||||
for i in xrange(0, len(self.task_xml)):
|
||||
for i in xrange(len(self.task_xml)):
|
||||
self.current_task_number = i
|
||||
self.setup_next_task(reset=True)
|
||||
self.current_task.reset(self.system)
|
||||
@@ -673,9 +726,10 @@ class CombinedOpenEndedV1Module():
|
||||
Output: The status html to be rendered
|
||||
"""
|
||||
status = []
|
||||
for i in xrange(0, self.current_task_number + 1):
|
||||
task_data = self.get_last_response(i)
|
||||
task_data.update({'task_number': i + 1})
|
||||
for i in xrange(0, len(self.task_xml)):
|
||||
human_task_name = self.extract_human_name_from_task(self.task_xml[i])
|
||||
|
||||
task_data = {'task_number': i + 1, 'human_task' : human_task_name, 'current' : self.current_task_number==i}
|
||||
status.append(task_data)
|
||||
|
||||
context = {
|
||||
|
||||
@@ -206,20 +206,49 @@ class CombinedOpenEndedRubric(object):
|
||||
def render_combined_rubric(self, rubric_xml, scores, score_types, feedback_types):
|
||||
success, score_tuples = CombinedOpenEndedRubric.reformat_scores_for_rendering(scores, score_types,
|
||||
feedback_types)
|
||||
#Get all the categories in the rubric
|
||||
rubric_categories = self.extract_categories(rubric_xml)
|
||||
#Get a list of max scores, each entry belonging to a rubric category
|
||||
max_scores = map((lambda cat: cat['options'][-1]['points']), rubric_categories)
|
||||
actual_scores = []
|
||||
#Get the highest possible score across all categories
|
||||
max_score = max(max_scores)
|
||||
for i in xrange(0, len(rubric_categories)):
|
||||
category = rubric_categories[i]
|
||||
for j in xrange(0, len(category['options'])):
|
||||
#Loop through each category
|
||||
for i, category in enumerate(rubric_categories):
|
||||
#Loop through each option in the category
|
||||
for j in xrange(len(category['options'])):
|
||||
#Intialize empty grader types list
|
||||
rubric_categories[i]['options'][j]['grader_types'] = []
|
||||
for tuple in score_tuples:
|
||||
if tuple[1] == i and tuple[2] == j:
|
||||
for grader_type in tuple[3]:
|
||||
#Score tuples are a flat data structure with (category, option, grader_type_list) for selected graders
|
||||
for tup in score_tuples:
|
||||
if tup[1] == i and tup[2] == j:
|
||||
for grader_type in tup[3]:
|
||||
#Set the rubric grader type to the tuple grader types
|
||||
rubric_categories[i]['options'][j]['grader_types'].append(grader_type)
|
||||
#Grab the score and add it to the actual scores. J will be the score for the selected
|
||||
#grader type
|
||||
if len(actual_scores)<=i:
|
||||
#Initialize a new list in the list of lists
|
||||
actual_scores.append([j])
|
||||
else:
|
||||
#If a list in the list of lists for this position exists, append to it
|
||||
actual_scores[i] += [j]
|
||||
|
||||
actual_scores = [sum(i) / len(i) for i in actual_scores]
|
||||
correct = []
|
||||
#Define if the student is "correct" (1) "incorrect" (0) or "partially correct" (.5)
|
||||
for (i, a) in enumerate(actual_scores):
|
||||
if int(a) == max_scores[i]:
|
||||
correct.append(1)
|
||||
elif int(a)==0:
|
||||
correct.append(0)
|
||||
else:
|
||||
correct.append(.5)
|
||||
|
||||
html = self.system.render_template('{0}/open_ended_combined_rubric.html'.format(self.TEMPLATE_DIR),
|
||||
{'categories': rubric_categories,
|
||||
'max_scores': max_scores,
|
||||
'correct' : correct,
|
||||
'has_score': True,
|
||||
'view_only': True,
|
||||
'max_score': max_score,
|
||||
|
||||
@@ -11,6 +11,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GradingServiceError(Exception):
|
||||
"""
|
||||
Exception for grading service. Shown when Open Response Assessment servers cannot be reached.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -62,7 +65,6 @@ class GradingService(object):
|
||||
"""
|
||||
Make a get request to the grading controller
|
||||
"""
|
||||
log.debug(params)
|
||||
op = lambda: self.session.get(url,
|
||||
allow_redirects=allow_redirects,
|
||||
params=params)
|
||||
|
||||
@@ -641,6 +641,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
"""
|
||||
# Once we close the problem, we should not allow students
|
||||
# to save answers
|
||||
error_message = ""
|
||||
closed, msg = self.check_if_closed()
|
||||
if closed:
|
||||
return msg
|
||||
@@ -650,17 +651,11 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
|
||||
# add new history element with answer and empty score and hint.
|
||||
success, data = self.append_image_to_student_answer(data)
|
||||
error_message = ""
|
||||
if success:
|
||||
success, allowed_to_submit, error_message = self.check_if_student_can_submit()
|
||||
if allowed_to_submit:
|
||||
data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer'])
|
||||
self.new_history_entry(data['student_answer'])
|
||||
self.send_to_grader(data['student_answer'], system)
|
||||
self.change_state(self.ASSESSING)
|
||||
else:
|
||||
# Error message already defined
|
||||
success = False
|
||||
data['student_answer'] = OpenEndedModule.sanitize_html(data['student_answer'])
|
||||
self.new_history_entry(data['student_answer'])
|
||||
self.send_to_grader(data['student_answer'], system)
|
||||
self.change_state(self.ASSESSING)
|
||||
else:
|
||||
# This is a student_facing_error
|
||||
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
|
||||
@@ -668,7 +663,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
return {
|
||||
'success': success,
|
||||
'error': error_message,
|
||||
'student_response': data['student_answer']
|
||||
'student_response': data['student_answer'].replace("\n","<br/>")
|
||||
}
|
||||
|
||||
def update_score(self, data, system):
|
||||
@@ -699,12 +694,12 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
|
||||
score = self.latest_score()
|
||||
correct = 'correct' if self.is_submission_correct(score) else 'incorrect'
|
||||
if self.child_state == self.ASSESSING:
|
||||
eta_string = self.get_eta()
|
||||
eta_string = "Your response has been submitted. Please check back later for your grade."
|
||||
else:
|
||||
post_assessment = ""
|
||||
correct = ""
|
||||
previous_answer = self.initial_display
|
||||
|
||||
previous_answer = ""
|
||||
previous_answer = previous_answer.replace("\n","<br/>")
|
||||
context = {
|
||||
'prompt': self.child_prompt,
|
||||
'previous_answer': previous_answer,
|
||||
|
||||
@@ -58,7 +58,7 @@ class OpenEndedChild(object):
|
||||
'assessing': 'In progress',
|
||||
'post_assessment': 'Done',
|
||||
'done': 'Done',
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, system, location, definition, descriptor, static_data,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
@@ -179,10 +179,11 @@ class OpenEndedChild(object):
|
||||
answer = autolink_html(answer)
|
||||
cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
|
||||
host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
|
||||
whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
|
||||
whitelist_tags=set(['embed', 'iframe', 'a', 'img', 'br']))
|
||||
clean_html = cleaner.clean_html(answer)
|
||||
clean_html = re.sub(r'</p>$', '', re.sub(r'^<p>', '', clean_html))
|
||||
except:
|
||||
clean_html = re.sub("\n","<br/>", clean_html)
|
||||
except Exception:
|
||||
clean_html = answer
|
||||
return clean_html
|
||||
|
||||
@@ -230,7 +231,7 @@ class OpenEndedChild(object):
|
||||
'max_score': self._max_score,
|
||||
'child_attempts': self.child_attempts,
|
||||
'child_created': False,
|
||||
}
|
||||
}
|
||||
return json.dumps(state)
|
||||
|
||||
def _allow_reset(self):
|
||||
@@ -332,7 +333,7 @@ class OpenEndedChild(object):
|
||||
try:
|
||||
image_data.seek(0)
|
||||
image_ok = open_ended_image_submission.run_image_tests(image_data)
|
||||
except:
|
||||
except Exception:
|
||||
log.exception("Could not create image and check it.")
|
||||
|
||||
if image_ok:
|
||||
@@ -345,7 +346,7 @@ class OpenEndedChild(object):
|
||||
success, s3_public_url = open_ended_image_submission.upload_to_s3(
|
||||
image_data, image_key, self.s3_interface
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
log.exception("Could not upload image to S3.")
|
||||
|
||||
return success, image_ok, s3_public_url
|
||||
@@ -434,38 +435,6 @@ class OpenEndedChild(object):
|
||||
|
||||
return success, string
|
||||
|
||||
def check_if_student_can_submit(self):
|
||||
location = self.location_string
|
||||
|
||||
student_id = self.system.anonymous_student_id
|
||||
success = False
|
||||
allowed_to_submit = True
|
||||
response = {}
|
||||
# This is a student_facing_error
|
||||
error_string = ("You need to peer grade {0} more in order to make another submission. "
|
||||
"You have graded {1}, and {2} are required. You have made {3} successful peer grading submissions.")
|
||||
try:
|
||||
response = self.peer_gs.get_data_for_location(self.location_string, student_id)
|
||||
count_graded = response['count_graded']
|
||||
count_required = response['count_required']
|
||||
student_sub_count = response['student_sub_count']
|
||||
success = True
|
||||
except:
|
||||
# This is a dev_facing_error
|
||||
log.error("Could not contact external open ended graders for location {0} and student {1}".format(
|
||||
self.location_string, student_id))
|
||||
# This is a student_facing_error
|
||||
error_message = "Could not contact the graders. Please notify course staff."
|
||||
return success, allowed_to_submit, error_message
|
||||
if count_graded >= count_required:
|
||||
return success, allowed_to_submit, ""
|
||||
else:
|
||||
allowed_to_submit = False
|
||||
# This is a student_facing_error
|
||||
error_message = error_string.format(count_required - count_graded, count_graded, count_required,
|
||||
student_sub_count)
|
||||
return success, allowed_to_submit, error_message
|
||||
|
||||
def get_eta(self):
|
||||
if self.controller_qs:
|
||||
response = self.controller_qs.check_for_eta(self.location_string)
|
||||
|
||||
@@ -124,4 +124,4 @@ class MockPeerGradingService(object):
|
||||
]}
|
||||
|
||||
def get_data_for_location(self, problem_location, student_id):
|
||||
return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1}
|
||||
return {"version": 1, "count_graded": 3, "count_required": 3, "success": True, "student_sub_count": 1, 'submissions_available' : 0}
|
||||
|
||||
@@ -61,6 +61,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
else:
|
||||
previous_answer = ''
|
||||
|
||||
previous_answer = previous_answer.replace("\n","<br/>")
|
||||
context = {
|
||||
'prompt': self.child_prompt,
|
||||
'previous_answer': previous_answer,
|
||||
@@ -184,14 +185,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
# add new history element with answer and empty score and hint.
|
||||
success, data = self.append_image_to_student_answer(data)
|
||||
if success:
|
||||
success, allowed_to_submit, error_message = self.check_if_student_can_submit()
|
||||
if allowed_to_submit:
|
||||
data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer'])
|
||||
self.new_history_entry(data['student_answer'])
|
||||
self.change_state(self.ASSESSING)
|
||||
else:
|
||||
# Error message already defined
|
||||
success = False
|
||||
data['student_answer'] = SelfAssessmentModule.sanitize_html(data['student_answer'])
|
||||
self.new_history_entry(data['student_answer'])
|
||||
self.change_state(self.ASSESSING)
|
||||
else:
|
||||
# This is a student_facing_error
|
||||
error_message = "There was a problem saving the image in your submission. Please try a different image, or try pasting a link to an image into the answer box."
|
||||
@@ -200,7 +196,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
'success': success,
|
||||
'rubric_html': self.get_rubric_html(system),
|
||||
'error': error_message,
|
||||
'student_response': data['student_answer'],
|
||||
'student_response': data['student_answer'].replace("\n","<br/>")
|
||||
}
|
||||
|
||||
def save_assessment(self, data, _system):
|
||||
@@ -272,8 +268,6 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
|
||||
try:
|
||||
rubric_scores = json.loads(latest_post_assessment)
|
||||
except:
|
||||
# This is a dev_facing_error
|
||||
log.error("Cannot parse rubric scores in self assessment module from {0}".format(latest_post_assessment))
|
||||
rubric_scores = []
|
||||
return [rubric_scores]
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ class PeerGradingFields(object):
|
||||
)
|
||||
due = Date(
|
||||
help="Due date that should be displayed.",
|
||||
default=None,
|
||||
scope=Scope.settings)
|
||||
graceperiod = Timedelta(
|
||||
help="Amount of grace to give on the due date.",
|
||||
@@ -189,9 +188,8 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
def query_data_for_location(self):
|
||||
def query_data_for_location(self, location):
|
||||
student_id = self.system.anonymous_student_id
|
||||
location = self.link_to_location
|
||||
success = False
|
||||
response = {}
|
||||
|
||||
@@ -229,7 +227,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
count_graded = self.student_data_for_location['count_graded']
|
||||
count_required = self.student_data_for_location['count_required']
|
||||
except:
|
||||
success, response = self.query_data_for_location()
|
||||
success, response = self.query_data_for_location(self.location)
|
||||
if not success:
|
||||
log.exception(
|
||||
"No instance data found and could not get data from controller for loc {0} student {1}".format(
|
||||
@@ -312,17 +310,26 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
error: if there was an error in the submission, this is the error message
|
||||
"""
|
||||
|
||||
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]', 'submission_flagged', 'answer_unknown'])
|
||||
success, message = self._check_required(data, required)
|
||||
required = ['location', 'submission_id', 'submission_key', 'score', 'feedback', 'submission_flagged', 'answer_unknown']
|
||||
if data.get("submission_flagged", False) in ["false", False, "False", "FALSE"]:
|
||||
required.append("rubric_scores[]")
|
||||
success, message = self._check_required(data, set(required))
|
||||
if not success:
|
||||
return self._err_response(message)
|
||||
|
||||
data_dict = {k:data.get(k) for k in required}
|
||||
data_dict['rubric_scores'] = data.getlist('rubric_scores[]')
|
||||
if 'rubric_scores[]' in required:
|
||||
data_dict['rubric_scores'] = data.getlist('rubric_scores[]')
|
||||
data_dict['grader_id'] = self.system.anonymous_student_id
|
||||
|
||||
try:
|
||||
response = self.peer_gs.save_grade(**data_dict)
|
||||
success, location_data = self.query_data_for_location(data_dict['location'])
|
||||
#Don't check for success above because the response = statement will raise the same Exception as the one
|
||||
#that will cause success to be false.
|
||||
response.update({'required_done' : False})
|
||||
if 'count_graded' in location_data and 'count_required' in location_data and int(location_data['count_graded'])>=int(location_data['count_required']):
|
||||
response['required_done'] = True
|
||||
return response
|
||||
except GradingServiceError:
|
||||
# This is a dev_facing_error
|
||||
@@ -502,7 +509,7 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
error_text = "Could not get list of problems to peer grade. Please notify course staff."
|
||||
log.error(error_text)
|
||||
success = False
|
||||
except:
|
||||
except Exception:
|
||||
log.exception("Could not contact peer grading service.")
|
||||
success = False
|
||||
|
||||
@@ -513,20 +520,24 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
'''
|
||||
try:
|
||||
return modulestore().get_instance(self.system.course_id, location)
|
||||
except:
|
||||
except Exception:
|
||||
# the linked problem doesn't exist
|
||||
log.error("Problem {0} does not exist in this course".format(location))
|
||||
raise
|
||||
|
||||
good_problem_list = []
|
||||
for problem in problem_list:
|
||||
problem_location = problem['location']
|
||||
descriptor = _find_corresponding_module_for_location(problem_location)
|
||||
try:
|
||||
descriptor = _find_corresponding_module_for_location(problem_location)
|
||||
except Exception:
|
||||
continue
|
||||
if descriptor:
|
||||
problem['due'] = descriptor.lms.due
|
||||
grace_period = descriptor.lms.graceperiod
|
||||
try:
|
||||
problem_timeinfo = TimeInfo(problem['due'], grace_period)
|
||||
except:
|
||||
except Exception:
|
||||
log.error("Malformed due date or grace period string for location {0}".format(problem_location))
|
||||
raise
|
||||
if self._closed(problem_timeinfo):
|
||||
@@ -537,13 +548,14 @@ class PeerGradingModule(PeerGradingFields, XModule):
|
||||
# if we can't find the due date, assume that it doesn't have one
|
||||
problem['due'] = None
|
||||
problem['closed'] = False
|
||||
good_problem_list.append(problem)
|
||||
|
||||
ajax_url = self.ajax_url
|
||||
html = self.system.render_template('peer_grading/peer_grading.html', {
|
||||
'course_id': self.system.course_id,
|
||||
'ajax_url': ajax_url,
|
||||
'success': success,
|
||||
'problem_list': problem_list,
|
||||
'problem_list': good_problem_list,
|
||||
'error_text': error_text,
|
||||
# Checked above
|
||||
'staff_access': False,
|
||||
|
||||
@@ -73,6 +73,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.openendedchild = OpenEndedChild(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
@@ -203,7 +204,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = get_test_system()
|
||||
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.test_system.location = self.location
|
||||
self.mock_xqueue = MagicMock()
|
||||
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
|
||||
@@ -410,6 +411,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
|
||||
descriptor = Mock(data=full_definition)
|
||||
test_system = get_test_system()
|
||||
test_system.open_ended_grading_interface = None
|
||||
combinedoe_container = CombinedOpenEndedModule(
|
||||
test_system,
|
||||
descriptor,
|
||||
@@ -536,6 +538,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.test_system.xqueue['interface'] = Mock(
|
||||
send_to_queue=Mock(side_effect=[1, "queued"])
|
||||
)
|
||||
@@ -569,9 +572,9 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
module = self.get_module_from_location(self.problem_location, COURSE)
|
||||
|
||||
#Simulate a student saving an answer
|
||||
module.handle_ajax("save_answer", {"student_answer": self.answer})
|
||||
status = module.handle_ajax("get_status", {})
|
||||
self.assertTrue(isinstance(status, basestring))
|
||||
html = module.handle_ajax("get_html", {})
|
||||
module.handle_ajax("save_answer", {"student_answer": self.answer, "can_upload_files" : False, "student_file" : None})
|
||||
html = module.handle_ajax("get_html", {})
|
||||
|
||||
#Mock a student submitting an assessment
|
||||
assessment_dict = MockQueryDict()
|
||||
@@ -579,8 +582,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
module.handle_ajax("save_assessment", assessment_dict)
|
||||
task_one_json = json.loads(module.task_states[0])
|
||||
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
|
||||
status = module.handle_ajax("get_status", {})
|
||||
self.assertTrue(isinstance(status, basestring))
|
||||
rubric = module.handle_ajax("get_combined_rubric", {})
|
||||
|
||||
#Move to the next step in the problem
|
||||
module.handle_ajax("next_problem", {})
|
||||
@@ -617,7 +619,6 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
module.handle_ajax("save_assessment", assessment_dict)
|
||||
task_one_json = json.loads(module.task_states[0])
|
||||
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
|
||||
module.handle_ajax("get_status", {})
|
||||
|
||||
#Move to the next step in the problem
|
||||
try:
|
||||
@@ -660,15 +661,11 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
|
||||
#Get html and other data client will request
|
||||
module.get_html()
|
||||
legend = module.handle_ajax("get_legend", {})
|
||||
self.assertTrue(isinstance(legend, basestring))
|
||||
|
||||
module.handle_ajax("get_status", {})
|
||||
module.handle_ajax("skip_post_assessment", {})
|
||||
self.assertTrue(isinstance(legend, basestring))
|
||||
|
||||
#Get all results
|
||||
module.handle_ajax("get_results", {})
|
||||
module.handle_ajax("get_combined_rubric", {})
|
||||
|
||||
#reset the problem
|
||||
module.handle_ajax("reset", {})
|
||||
@@ -686,6 +683,7 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.test_system.xqueue['interface'] = Mock(
|
||||
send_to_queue=Mock(side_effect=[1, "queued"])
|
||||
)
|
||||
@@ -702,8 +700,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
|
||||
|
||||
#Simulate a student saving an answer
|
||||
module.handle_ajax("save_answer", {"student_answer": self.answer})
|
||||
status = module.handle_ajax("get_status", {})
|
||||
self.assertTrue(isinstance(status, basestring))
|
||||
|
||||
#Mock a student submitting an assessment
|
||||
assessment_dict = MockQueryDict()
|
||||
@@ -711,8 +707,6 @@ class OpenEndedModuleXmlAttemptTest(unittest.TestCase, DummyModulestore):
|
||||
module.handle_ajax("save_assessment", assessment_dict)
|
||||
task_one_json = json.loads(module.task_states[0])
|
||||
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
|
||||
status = module.handle_ajax("get_status", {})
|
||||
self.assertTrue(isinstance(status, basestring))
|
||||
|
||||
#Move to the next step in the problem
|
||||
module.handle_ajax("next_problem", {})
|
||||
|
||||
@@ -89,12 +89,6 @@ class RoundTripTestCase(unittest.TestCase):
|
||||
print("Checking module equality")
|
||||
for location in initial_import.modules[course_id].keys():
|
||||
print("Checking", location)
|
||||
if location.category == 'html':
|
||||
print(
|
||||
"Skipping html modules--they can't import in"
|
||||
" final form without writing files..."
|
||||
)
|
||||
continue
|
||||
self.assertEquals(initial_import.modules[course_id][location],
|
||||
second_import.modules[course_id][location])
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
|
||||
Try getting data from the external grading service
|
||||
@return:
|
||||
"""
|
||||
success, data = self.peer_grading.query_data_for_location()
|
||||
success, data = self.peer_grading.query_data_for_location(self.problem_location.url())
|
||||
self.assertEqual(success, True)
|
||||
|
||||
def test_get_score(self):
|
||||
|
||||
2
common/test/data/simple_with_draft/README.md
Normal file
2
common/test/data/simple_with_draft/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
This is a simple, but non-trivial, course using multiple module types and some nested structure.
|
||||
|
||||
31
common/test/data/simple_with_draft/course.xml
Normal file
31
common/test/data/simple_with_draft/course.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<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>
|
||||
3
common/test/data/simple_with_draft/html/toylab.html
Normal file
3
common/test/data/simple_with_draft/html/toylab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<p>Isn't the toy course great?</p>
|
||||
43
common/test/data/simple_with_draft/problem/L1_Problem_1.xml
Normal file
43
common/test/data/simple_with_draft/problem/L1_Problem_1.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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>
|
||||
62
common/test/data/simple_with_draft/problem/ps01-simple.xml
Normal file
62
common/test/data/simple_with_draft/problem/ps01-simple.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<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>
|
||||
1
common/test/data/test_import_course/about/end_date.html
Normal file
1
common/test/data/test_import_course/about/end_date.html
Normal file
@@ -0,0 +1 @@
|
||||
TBD
|
||||
@@ -0,0 +1,3 @@
|
||||
<sequential>
|
||||
<sequential filename='vertical_sequential' slug='vertical_sequential' />
|
||||
</sequential>
|
||||
1
common/test/data/test_import_course/course.xml
Normal file
1
common/test/data/test_import_course/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course org="edX" course="test_import_course" url_name="2012_Fall"/>
|
||||
20
common/test/data/test_import_course/course/2012_Fall.xml
Normal file
20
common/test/data/test_import_course/course/2012_Fall.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<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>
|
||||
1
common/test/data/test_import_course/info/handouts.html
Normal file
1
common/test/data/test_import_course/info/handouts.html
Normal file
@@ -0,0 +1 @@
|
||||
<a href='/static/handouts/sample_handout.txt'>Sample</a>
|
||||
33
common/test/data/test_import_course/policies/2012_Fall.json
Normal file
33
common/test/data/test_import_course/policies/2012_Fall.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<sequential>
|
||||
<vertical filename="vertical_test" slug="vertical_test" />
|
||||
<html slug="unicode">…</html>
|
||||
</sequential>
|
||||
@@ -0,0 +1 @@
|
||||
<p>this file should be in the contentstore</p>
|
||||
@@ -0,0 +1,9 @@
|
||||
<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>
|
||||
@@ -0,0 +1 @@
|
||||
<video display_name="default" youtube_id_0_75="JMD_ifUUfsU" youtube_id_1_0="OEoXaMPEzfM" youtube_id_1_25="AKqURZnYqpk" youtube_id_1_5="DYpADpL7jAY" name="sample_video"/>
|
||||
@@ -8,6 +8,8 @@
|
||||
<html url_name="nonportable"/>
|
||||
<html url_name="nonportable_link"/>
|
||||
<html url_name="badlink"/>
|
||||
<html url_name="with_styling"/>
|
||||
<html url_name="just_img"/>
|
||||
<video url_name="Video_Resources" youtube_id_1_0="1bK-WdDi6Qw" display_name="Video Resources"/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube_id_1_0="p2Q6BrNhdh8" display_name="Welcome"/>
|
||||
|
||||
1
common/test/data/toy/html/just_img.html
Normal file
1
common/test/data/toy/html/just_img.html
Normal file
@@ -0,0 +1 @@
|
||||
<img src="/static/foo_bar.jpg" />
|
||||
1
common/test/data/toy/html/just_img.xml
Normal file
1
common/test/data/toy/html/just_img.xml
Normal file
@@ -0,0 +1 @@
|
||||
<html filename="just_img.html"/>
|
||||
1
common/test/data/toy/html/with_styling.html
Normal file
1
common/test/data/toy/html/with_styling.html
Normal file
@@ -0,0 +1 @@
|
||||
<p style="font:italic bold 72px/30px Georgia, serif; color: red; ">Red text here</p>
|
||||
1
common/test/data/toy/html/with_styling.xml
Normal file
1
common/test/data/toy/html/with_styling.xml
Normal file
@@ -0,0 +1 @@
|
||||
<html filename="with_styling.html"/>
|
||||
@@ -136,6 +136,25 @@ To run a single nose test:
|
||||
|
||||
nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
|
||||
|
||||
To run a single test and get stdout, with proper env config:
|
||||
|
||||
python manage.py cms --settings test test contentstore.tests.test_import_nostatic -s
|
||||
|
||||
To run a single test and get stdout and get coverage:
|
||||
|
||||
python -m coverage run --rcfile=./common/lib/xmodule/.coveragerc which ./manage.py cms --settings test test --traceback --logging-clear-handlers --liveserver=localhost:8000-9000 contentstore.tests.test_import_nostatic -s # cms example
|
||||
python -m coverage run --rcfile=./lms/.coveragerc which ./manage.py lms --settings test test --traceback --logging-clear-handlers --liveserver=localhost:8000-9000 courseware.tests.test_module_render -s # lms example
|
||||
|
||||
generate coverage report:
|
||||
|
||||
coverage report --rcfile=./common/lib/xmodule/.coveragerc
|
||||
|
||||
or to get html report:
|
||||
|
||||
coverage html --rcfile=./common/lib/xmodule/.coveragerc
|
||||
|
||||
then browse reports/common/lib/xmodule/cover/index.html
|
||||
|
||||
|
||||
Very handy: if you uncomment the `pdb=1` line in `setup.cfg`, it will drop you into pdb on error. This lets you go up and down the stack and see what the values of the variables are. Check out [the pdb documentation](http://docs.python.org/library/pdb.html)
|
||||
|
||||
|
||||
@@ -30,10 +30,22 @@ TESTS_FAILED=0
|
||||
# /usr/bin/Xvfb :1 -screen 0 1024x268x24
|
||||
# This allows us to run Chrome without a display
|
||||
export DISPLAY=:1
|
||||
SKIP_TESTS=""
|
||||
|
||||
# Testing for the existance of these environment variables
|
||||
if [ ! -z ${LETTUCE_BROWSER+x} ]; then
|
||||
SKIP_TESTS="--tag -skip_$LETTUCE_BROWSER"
|
||||
fi
|
||||
if [ ! -z ${SAUCE_ENABLED+x} ]; then
|
||||
# SAUCE_INFO is a - seperated string PLATFORM-BROWSER-VERSION-DEVICE
|
||||
# Error checking is done in the setting up of the browser
|
||||
IFS='-' read -a SAUCE <<< "${SAUCE_INFO}"
|
||||
SKIP_TESTS="--tag -skip_sauce --tag -skip_${SAUCE[1]}"
|
||||
fi
|
||||
|
||||
# Run the lms and cms acceptance tests
|
||||
# (the -v flag turns off color in the output)
|
||||
rake test_acceptance_lms["-v 3"] || TESTS_FAILED=1
|
||||
rake test_acceptance_cms["-v 3"] || TESTS_FAILED=1
|
||||
rake test_acceptance_lms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
|
||||
rake test_acceptance_cms["-v 3 $SKIP_TESTS"] || TESTS_FAILED=1
|
||||
|
||||
[ $TESTS_FAILED == '0' ]
|
||||
|
||||
@@ -3,21 +3,18 @@ from django.test.utils import override_settings
|
||||
|
||||
import xmodule.modulestore.django
|
||||
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_XML_MODULESTORE
|
||||
from courseware.tests.tests import LoginEnrollmentTestCase
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE)
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class WikiRedirectTestCase(LoginEnrollmentTestCase):
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
def find_course(name):
|
||||
"""Assumes the course is present"""
|
||||
return [c for c in courses if c.location.course == name][0]
|
||||
|
||||
self.toy = find_course("toy")
|
||||
# Load the toy course
|
||||
self.toy = modulestore().get_course('edX/toy/2012_Fall')
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
|
||||
@@ -81,8 +81,8 @@ def get_opt_course_with_access(user, course_id, action):
|
||||
def course_image_url(course):
|
||||
"""Try to look up the image url for the course. If it's not found,
|
||||
log an error and return the dead link"""
|
||||
if modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
|
||||
return '/static/' + course.data_dir + "/images/course_image.jpg"
|
||||
if course.lms.static_asset_path or modulestore().get_modulestore_type(course.location.course_id) == XML_MODULESTORE_TYPE:
|
||||
return '/static/' + (course.lms.static_asset_path or getattr(course, 'data_dir', '')) + "/images/course_image.jpg"
|
||||
else:
|
||||
loc = course.location._replace(tag='c4x', category='asset', name=course.course_image)
|
||||
_path = StaticContent.get_url_path_from_location(loc)
|
||||
@@ -156,7 +156,8 @@ def get_course_about_section(course, section_key):
|
||||
model_data_cache,
|
||||
course.id,
|
||||
not_found_ok=True,
|
||||
wrap_xmodule_display=False
|
||||
wrap_xmodule_display=False,
|
||||
static_asset_path=course.lms.static_asset_path
|
||||
)
|
||||
|
||||
html = ''
|
||||
@@ -204,7 +205,8 @@ def get_course_info_section(request, course, section_key):
|
||||
loc,
|
||||
model_data_cache,
|
||||
course.id,
|
||||
wrap_xmodule_display=False
|
||||
wrap_xmodule_display=False,
|
||||
static_asset_path=course.lms.static_asset_path
|
||||
)
|
||||
|
||||
html = ''
|
||||
@@ -242,7 +244,8 @@ def get_course_syllabus_section(course, section_key):
|
||||
return replace_static_urls(
|
||||
htmlFile.read().decode('utf-8'),
|
||||
getattr(course, 'data_dir', None),
|
||||
course_id=course.location.course_id
|
||||
course_id=course.location.course_id,
|
||||
static_asset_path=course.lms.static_asset_path,
|
||||
)
|
||||
except ResourceNotFoundError:
|
||||
log.exception("Missing syllabus section {key} in course {url}".format(
|
||||
|
||||
@@ -11,7 +11,8 @@ Feature: Login in as a registered user
|
||||
And I submit my credentials on the login form
|
||||
Then I should see the login error message "This account has not been activated"
|
||||
|
||||
# CHROME ONLY, firefox will not redirect properly
|
||||
# firefox will not redirect properly when the whole suite is run
|
||||
@skip_firefox
|
||||
Scenario: Login to an activated account
|
||||
Given I am an edX user
|
||||
And I am an activated user
|
||||
|
||||
@@ -3,7 +3,8 @@ Feature: Sign in
|
||||
As a new user
|
||||
I want to signup for a student account
|
||||
|
||||
# CHROME ONLY, firefox will not redirect properly
|
||||
# firefox will not redirect properly
|
||||
@skip_firefox
|
||||
Scenario: Sign up from the homepage
|
||||
Given I visit the homepage
|
||||
When I click the link with the text "Register Now"
|
||||
|
||||
@@ -11,6 +11,8 @@ Feature: Video component
|
||||
Given the course has a Video component in Youtube mode
|
||||
Then when I view the video it has rendered in Youtube mode
|
||||
|
||||
# Firefox doesn't have HTML5
|
||||
@skip_firefox
|
||||
Scenario: Autoplay is enabled in LMS for a Video component
|
||||
Given the course has a Video component in HTML5 mode
|
||||
Then when I view the video it has autoplay enabled
|
||||
Then when I view the video it has autoplay enabled
|
||||
|
||||
@@ -124,7 +124,8 @@ def toc_for_course(user, request, course, active_chapter, active_section, model_
|
||||
|
||||
def get_module(user, request, location, model_data_cache, course_id,
|
||||
position=None, not_found_ok=False, wrap_xmodule_display=True,
|
||||
grade_bucket_type=None, depth=0):
|
||||
grade_bucket_type=None, depth=0,
|
||||
static_asset_path=''):
|
||||
"""
|
||||
Get an instance of the xmodule class identified by location,
|
||||
setting the state based on an existing StudentModule, or creating one if none
|
||||
@@ -141,6 +142,10 @@ def get_module(user, request, location, model_data_cache, course_id,
|
||||
position within module
|
||||
- depth : number of levels of descendents to cache when loading this module.
|
||||
None means cache all descendents
|
||||
- static_asset_path : static asset path to use (overrides descriptor's value); needed
|
||||
by get_course_info_section, because info section modules
|
||||
do not have a course as the parent module, and thus do not
|
||||
inherit this lms key value.
|
||||
|
||||
Returns: xmodule instance, or None if the user does not have access to the
|
||||
module. If there's an error, will try to return an instance of ErrorModule
|
||||
@@ -152,7 +157,8 @@ def get_module(user, request, location, model_data_cache, course_id,
|
||||
return get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
|
||||
position=position,
|
||||
wrap_xmodule_display=wrap_xmodule_display,
|
||||
grade_bucket_type=grade_bucket_type)
|
||||
grade_bucket_type=grade_bucket_type,
|
||||
static_asset_path=static_asset_path)
|
||||
except ItemNotFoundError:
|
||||
if not not_found_ok:
|
||||
log.exception("Error in get_module")
|
||||
@@ -179,7 +185,8 @@ def get_xqueue_callback_url_prefix(request):
|
||||
|
||||
|
||||
def get_module_for_descriptor(user, request, descriptor, model_data_cache, course_id,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path=''):
|
||||
"""
|
||||
Implements get_module, extracting out the request-specific functionality.
|
||||
|
||||
@@ -194,12 +201,14 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
|
||||
|
||||
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
|
||||
track_function, xqueue_callback_url_prefix,
|
||||
position, wrap_xmodule_display, grade_bucket_type)
|
||||
position, wrap_xmodule_display, grade_bucket_type,
|
||||
static_asset_path)
|
||||
|
||||
|
||||
def get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
|
||||
track_function, xqueue_callback_url_prefix,
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None):
|
||||
position=None, wrap_xmodule_display=True, grade_bucket_type=None,
|
||||
static_asset_path=''):
|
||||
"""
|
||||
Actually implement get_module, without requiring a request.
|
||||
|
||||
@@ -282,7 +291,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
|
||||
# inner_get_module, not the parent's callback. Add it as an argument....
|
||||
return get_module_for_descriptor_internal(user, descriptor, model_data_cache, course_id,
|
||||
track_function, make_xqueue_callback,
|
||||
position, wrap_xmodule_display, grade_bucket_type)
|
||||
position, wrap_xmodule_display, grade_bucket_type,
|
||||
static_asset_path)
|
||||
|
||||
def xblock_model_data(descriptor):
|
||||
return DbModel(
|
||||
@@ -349,6 +359,7 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
|
||||
static_replace.replace_static_urls,
|
||||
data_directory=getattr(descriptor, 'data_dir', None),
|
||||
course_id=course_id,
|
||||
static_asset_path=static_asset_path or descriptor.lms.static_asset_path,
|
||||
),
|
||||
replace_course_urls=partial(
|
||||
static_replace.replace_course_urls,
|
||||
@@ -407,7 +418,8 @@ def get_module_for_descriptor_internal(user, descriptor, model_data_cache, cours
|
||||
module.get_html = replace_static_urls(
|
||||
_get_html,
|
||||
getattr(descriptor, 'data_dir', None),
|
||||
course_id=course_id
|
||||
course_id=course_id,
|
||||
static_asset_path=static_asset_path or descriptor.lms.static_asset_path
|
||||
)
|
||||
|
||||
# Allow URLs of the form '/course/' refer to the root of multicourse directory
|
||||
|
||||
@@ -380,7 +380,8 @@ def get_static_tab_contents(request, course, tab):
|
||||
loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug'])
|
||||
model_data_cache = ModelDataCache.cache_for_descriptor_descendents(course.id,
|
||||
request.user, modulestore().get_instance(course.id, loc), depth=0)
|
||||
tab_module = get_module(request.user, request, loc, model_data_cache, course.id)
|
||||
tab_module = get_module(request.user, request, loc, model_data_cache, course.id,
|
||||
static_asset_path=course.lms.static_asset_path)
|
||||
|
||||
logging.debug('course_module = {0}'.format(tab_module))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.test.client import Client
|
||||
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class BaseTestXmodule(ModuleStoreTestCase):
|
||||
"""Base class for testing Xmodules with mongo store.
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
from xmodule.modulestore.tests.django_utils import xml_store_config, mongo_store_config, draft_mongo_store_config
|
||||
"""
|
||||
Define test configuration for modulestores.
|
||||
"""
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import xml_store_config, \
|
||||
mongo_store_config, draft_mongo_store_config,\
|
||||
mixed_store_config
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -6,3 +12,15 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_MONGO_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
TEST_DATA_DRAFT_MONGO_MODULESTORE = draft_mongo_store_config(TEST_DATA_DIR)
|
||||
|
||||
# Map all XML course fixtures so they are accessible through
|
||||
# the MixedModuleStore
|
||||
MAPPINGS = {
|
||||
'edX/toy/2012_Fall': 'xml',
|
||||
'edX/toy/TT_2012_Fall': 'xml',
|
||||
'edX/test_end/2012_Fall': 'xml',
|
||||
'edX/test_about_blob_end_date/2012_Fall': 'xml',
|
||||
'edX/graded/2012_Fall': 'xml',
|
||||
'edX/open_ended/2012_Fall': 'xml',
|
||||
}
|
||||
TEST_DATA_MIXED_MODULESTORE = mixed_store_config(TEST_DATA_DIR, MAPPINGS)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user