Merge branch 'master' into jonahstanley/add-courseteam-tests
This commit is contained in:
4
AUTHORS
4
AUTHORS
@@ -75,4 +75,6 @@ Frances Botsford <frances@edx.org>
|
||||
Jonah Stanley <Jonah_Stanley@brown.edu>
|
||||
Slater Victoroff <slater.r.victoroff@gmail.com>
|
||||
Peter Fogg <peter.p.fogg@gmail.com>
|
||||
Renzo Lucioni <renzolucioni@gmail.com>
|
||||
Bethany LaPenta <lapentab@mit.edu>
|
||||
Renzo Lucioni <renzolucioni@gmail.com>
|
||||
Felix Sun <felixsun@mit.edu>
|
||||
|
||||
@@ -5,6 +5,27 @@ These are notable changes in edx-platform. This is a rolling list of changes,
|
||||
in roughly chronological order, most recent first. Add your entries at or near
|
||||
the top. Include a label indicating the component affected.
|
||||
|
||||
Studio: Remove XML from the video component editor. All settings are
|
||||
moved to be edited as metadata.
|
||||
|
||||
XModule: Only write out assets files if the contents have changed.
|
||||
|
||||
XModule: Don't delete generated xmodule asset files when compiling (for
|
||||
instance, when XModule provides a coffeescript file, don't delete
|
||||
the associated javascript)
|
||||
|
||||
Studio: For courses running on edx.org (marketing site), disable fields in
|
||||
Course Settings that do not apply.
|
||||
|
||||
Common: Make asset watchers run as singletons (so they won't start if the
|
||||
watcher is already running in another shell).
|
||||
|
||||
Common: Use coffee directly when watching for coffeescript file changes.
|
||||
|
||||
Common: Make rake provide better error messages if packages are missing.
|
||||
|
||||
Common: Repairs development documentation generation by sphinx.
|
||||
|
||||
LMS: Problem rescoring. Added options on the Grades tab of the
|
||||
Instructor Dashboard to allow all students' submissions for a
|
||||
particular problem to be rescored. Also supports resetting all
|
||||
@@ -12,6 +33,8 @@ students' number of attempts to zero. Provides a list of background
|
||||
tasks that are currently running for the course, and an option to
|
||||
see a history of background tasks for a given problem.
|
||||
|
||||
LMS: Fixed the preferences scope for storing data in xmodules.
|
||||
|
||||
LMS: Forums. Added handling for case where discussion module can get `None` as
|
||||
value of lms.start in `lms/djangoapps/django_comment_client/utils.py`
|
||||
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -4,3 +4,4 @@ gem 'sass', '3.1.15'
|
||||
gem 'bourbon', '~> 1.3.6'
|
||||
gem 'colorize', '~> 0.5.8'
|
||||
gem 'launchy', '~> 2.1.2'
|
||||
gem 'sys-proctable', '~> 0.9.3'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches
|
||||
from nose.tools import assert_false, assert_equal, assert_regexp_matches, assert_true
|
||||
from common import type_in_codemirror
|
||||
|
||||
KEY_CSS = '.key input.policy-key'
|
||||
@@ -28,7 +28,15 @@ def i_am_on_advanced_course_settings(step):
|
||||
@step(u'I press the "([^"]*)" notification button$')
|
||||
def press_the_notification_button(step, name):
|
||||
css = 'a.%s-button' % name.lower()
|
||||
world.css_click(css)
|
||||
|
||||
# Save was clicked if either the save notification bar is gone, or we have a error notification
|
||||
# overlaying it (expected in the case of typing Object into display_name).
|
||||
def save_clicked():
|
||||
confirmation_dismissed = world.is_css_not_present('.is-shown.wrapper-notification-warning')
|
||||
error_showing = world.is_css_present('.is-shown.wrapper-notification-error')
|
||||
return confirmation_dismissed or error_showing
|
||||
|
||||
assert_true(world.css_click(css, success_condition=save_clicked), 'Save button not clicked after 5 attempts.')
|
||||
|
||||
|
||||
@step(u'I edit the value of a policy key$')
|
||||
|
||||
@@ -174,6 +174,16 @@ def open_new_unit(step):
|
||||
world.css_click('a.new-unit-item')
|
||||
|
||||
|
||||
@step('when I view the video it (.*) show the captions')
|
||||
def shows_captions(step, show_captions):
|
||||
# Prevent cookies from overriding course settings
|
||||
world.browser.cookies.delete('hide_captions')
|
||||
if show_captions == 'does not':
|
||||
assert world.css_find('.video')[0].has_class('closed')
|
||||
else:
|
||||
assert world.is_css_not_present('.video.closed')
|
||||
|
||||
|
||||
def type_in_codemirror(index, text):
|
||||
world.css_click(".CodeMirror", index=index)
|
||||
g = world.css_find("div.CodeMirror.CodeMirror-focused > div > textarea")
|
||||
|
||||
53
cms/djangoapps/contentstore/features/grading.feature
Normal file
53
cms/djangoapps/contentstore/features/grading.feature
Normal file
@@ -0,0 +1,53 @@
|
||||
Feature: Course Grading
|
||||
As a course author, I want to be able to configure how my course is graded
|
||||
|
||||
Scenario: Users can add grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
When I add "1" new grade
|
||||
Then I see I now have "3" grades
|
||||
|
||||
Scenario: Users can only have up to 5 grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
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
|
||||
Scenario: Users can delete grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
When I add "1" new grade
|
||||
And I delete a grade
|
||||
Then I see I now have "2" grades
|
||||
|
||||
Scenario: Users can move grading ranges
|
||||
Given I have opened a new course in Studio
|
||||
And I am viewing the grading settings
|
||||
When I move a grading section
|
||||
Then I see that the grade range has changed
|
||||
|
||||
Scenario: Users can modify Assignment types
|
||||
Given I have opened a new course in Studio
|
||||
And I have populated the course
|
||||
And I am viewing the grading settings
|
||||
When I change assignment type "Homework" to "New Type"
|
||||
And I go back to the main course page
|
||||
Then I do see the assignment name "New Type"
|
||||
And I do not see the assignment name "Homework"
|
||||
|
||||
Scenario: Users can delete Assignment types
|
||||
Given I have opened a new course in Studio
|
||||
And I have populated the course
|
||||
And I am viewing the grading settings
|
||||
When I delete the assignment type "Homework"
|
||||
And I go back to the main course page
|
||||
Then I do not see the assignment name "Homework"
|
||||
|
||||
Scenario: Users can add Assignment types
|
||||
Given I have opened a new course in Studio
|
||||
And I have populated the course
|
||||
And I am viewing the grading settings
|
||||
When I add a new assignment type "New Type"
|
||||
And I go back to the main course page
|
||||
Then I do see the assignment name "New Type"
|
||||
108
cms/djangoapps/contentstore/features/grading.py
Normal file
108
cms/djangoapps/contentstore/features/grading.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from common import *
|
||||
|
||||
|
||||
@step(u'I am viewing the grading settings')
|
||||
def view_grading_settings(step):
|
||||
world.click_course_settings()
|
||||
link_css = 'li.nav-course-settings-grading a'
|
||||
world.css_click(link_css)
|
||||
|
||||
|
||||
@step(u'I add "([^"]*)" new grade')
|
||||
def add_grade(step, many):
|
||||
grade_css = '.new-grade-button'
|
||||
for i in range(int(many)):
|
||||
world.css_click(grade_css)
|
||||
|
||||
|
||||
@step(u'I delete a grade')
|
||||
def delete_grade(step):
|
||||
#grade_css = 'li.grade-specific-bar > a.remove-button'
|
||||
#range_css = '.grade-specific-bar'
|
||||
#world.css_find(range_css)[1].mouseover()
|
||||
#world.css_click(grade_css)
|
||||
world.browser.execute_script('document.getElementsByClassName("remove-button")[0].click()')
|
||||
|
||||
|
||||
@step(u'I see I now have "([^"]*)" grades$')
|
||||
def view_grade_slider(step, how_many):
|
||||
grade_slider_css = '.grade-specific-bar'
|
||||
all_grades = world.css_find(grade_slider_css)
|
||||
assert len(all_grades) == int(how_many)
|
||||
|
||||
|
||||
@step(u'I move a grading section')
|
||||
def move_grade_slider(step):
|
||||
moveable_css = '.ui-resizable-e'
|
||||
f = world.css_find(moveable_css).first
|
||||
f.action_chains.drag_and_drop_by_offset(f._element, 100, 0).perform()
|
||||
|
||||
|
||||
@step(u'I see that the grade range has changed')
|
||||
def confirm_change(step):
|
||||
range_css = '.range'
|
||||
all_ranges = world.css_find(range_css)
|
||||
for i in range(len(all_ranges)):
|
||||
assert all_ranges[i].html != '0-50'
|
||||
|
||||
|
||||
@step(u'I change assignment type "([^"]*)" to "([^"]*)"$')
|
||||
def change_assignment_name(step, old_name, new_name):
|
||||
name_id = '#course-grading-assignment-name'
|
||||
index = get_type_index(old_name)
|
||||
f = world.css_find(name_id)[index]
|
||||
assert index != -1
|
||||
for count in range(len(old_name)):
|
||||
f._element.send_keys(Keys.END, Keys.BACK_SPACE)
|
||||
f._element.send_keys(new_name)
|
||||
|
||||
|
||||
@step(u'I go back to the main course page')
|
||||
def main_course_page(step):
|
||||
main_page_link_css = 'a[href="/MITx/999/course/Robot_Super_Course"]'
|
||||
world.css_click(main_page_link_css)
|
||||
|
||||
|
||||
@step(u'I do( not)? see the assignment name "([^"]*)"$')
|
||||
def see_assignment_name(step, do_not, name):
|
||||
assignment_menu_css = 'ul.menu > li > a'
|
||||
assignment_menu = world.css_find(assignment_menu_css)
|
||||
allnames = [item.html for item in assignment_menu]
|
||||
if do_not:
|
||||
assert not name in allnames
|
||||
else:
|
||||
assert name in allnames
|
||||
|
||||
|
||||
@step(u'I delete the assignment type "([^"]*)"$')
|
||||
def delete_assignment_type(step, to_delete):
|
||||
delete_css = '.remove-grading-data'
|
||||
world.css_click(delete_css, index=get_type_index(to_delete))
|
||||
|
||||
|
||||
@step(u'I add a new assignment type "([^"]*)"$')
|
||||
def add_assignment_type(step, new_name):
|
||||
add_button_css = '.add-grading-data'
|
||||
world.css_click(add_button_css)
|
||||
name_id = '#course-grading-assignment-name'
|
||||
f = world.css_find(name_id)[4]
|
||||
f._element.send_keys(new_name)
|
||||
|
||||
|
||||
@step(u'I have populated the course')
|
||||
def populate_course(step):
|
||||
step.given('I have added a new section')
|
||||
step.given('I have added a new subsection')
|
||||
|
||||
|
||||
def get_type_index(name):
|
||||
name_id = '#course-grading-assignment-name'
|
||||
f = world.css_find(name_id)
|
||||
for i in range(len(f)):
|
||||
if f[i].value == name:
|
||||
return i
|
||||
return -1
|
||||
@@ -4,10 +4,20 @@ Feature: Video Component Editor
|
||||
Scenario: User can view metadata
|
||||
Given I have created a Video component
|
||||
And I edit and select Settings
|
||||
Then I see only the Video display name setting
|
||||
Then I see the correct settings and default values
|
||||
|
||||
Scenario: User can modify display name
|
||||
Given I have created a Video component
|
||||
And I edit and select Settings
|
||||
Then I can modify the display name
|
||||
And my display name change is persisted on save
|
||||
|
||||
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
|
||||
|
||||
Scenario: Captions are shown when "show captions" is true
|
||||
Given I have created a Video component
|
||||
And I have set "show captions" to True
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
from lettuce import world, step
|
||||
|
||||
|
||||
@step('I see only the video display name setting$')
|
||||
def i_see_only_the_video_display_name(step):
|
||||
world.verify_all_setting_entries([['Display Name', "default", True]])
|
||||
@step('I see the correct settings and default values$')
|
||||
def i_see_the_correct_settings_and_values(step):
|
||||
world.verify_all_setting_entries([['Default Speed', 'OEoXaMPEzfM', False],
|
||||
['Display Name', 'default', True],
|
||||
['Download Track', '', False],
|
||||
['Download Video', '', False],
|
||||
['Show Captions', 'True', False],
|
||||
['Speed: .75x', '', False],
|
||||
['Speed: 1.25x', '', False],
|
||||
['Speed: 1.5x', '', False]])
|
||||
|
||||
|
||||
@step('I have set "show captions" to (.*)')
|
||||
def set_show_captions(step, setting):
|
||||
world.css_click('a.edit-button')
|
||||
world.browser.select('Show Captions', setting)
|
||||
world.css_click('a.save-button')
|
||||
|
||||
@@ -9,7 +9,16 @@ Feature: Video Component
|
||||
Given I have clicked the new unit button
|
||||
Then creating a video takes a single click
|
||||
|
||||
Scenario: Captions are shown correctly
|
||||
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
|
||||
|
||||
Scenario: Captions are shown correctly
|
||||
Given I have created a Video component
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
Scenario: Captions are toggled correctly
|
||||
Given I have created a Video component
|
||||
And I have toggled captions
|
||||
Then when I view the video it does show the captions
|
||||
|
||||
@@ -18,11 +18,16 @@ def video_takes_a_single_click(_step):
|
||||
assert(world.is_css_present('.xmodule_VideoModule'))
|
||||
|
||||
|
||||
@step('I have hidden captions')
|
||||
def set_show_captions_false(step):
|
||||
world.css_click('a.hide-subtitles')
|
||||
|
||||
|
||||
@step('when I view the video it does not show the captions')
|
||||
def does_not_show_captions(step):
|
||||
assert world.css_find('.video')[0].has_class('closed')
|
||||
@step('I have (hidden|toggled) captions')
|
||||
def hide_or_show_captions(step, shown):
|
||||
button_css = 'a.hide-subtitles'
|
||||
if shown == 'hidden':
|
||||
world.css_click(button_css)
|
||||
if shown == 'toggled':
|
||||
world.css_click(button_css)
|
||||
# When we click the first time, a tooltip shows up. We want to
|
||||
# click the button rather than the tooltip, so move the mouse
|
||||
# away to make it disappear.
|
||||
button = world.css_find(button_css)
|
||||
button.mouse_out()
|
||||
world.css_click(button_css)
|
||||
|
||||
@@ -19,7 +19,6 @@ class ChecklistTestCase(CourseTestCase):
|
||||
modulestore = get_modulestore(self.course.location)
|
||||
return modulestore.get_item(self.course.location).checklists
|
||||
|
||||
|
||||
def compare_checklists(self, persisted, request):
|
||||
"""
|
||||
Handles url expansion as possible difference and descends into guts
|
||||
@@ -99,7 +98,6 @@ class ChecklistTestCase(CourseTestCase):
|
||||
'name': self.course.location.name,
|
||||
'checklist_index': 2})
|
||||
|
||||
|
||||
def get_first_item(checklist):
|
||||
return checklist['items'][0]
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"""
|
||||
Tests for Studio Course Settings.
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import copy
|
||||
import mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import UTC
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
|
||||
@@ -21,6 +26,9 @@ from xmodule.fields import Date
|
||||
|
||||
|
||||
class CourseTestCase(ModuleStoreTestCase):
|
||||
"""
|
||||
Base class for test classes below.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
These tests need a user in the DB so that the django Test Client
|
||||
@@ -51,6 +59,9 @@ class CourseTestCase(ModuleStoreTestCase):
|
||||
|
||||
|
||||
class CourseDetailsTestCase(CourseTestCase):
|
||||
"""
|
||||
Tests the first course settings page (course dates, overview, etc.).
|
||||
"""
|
||||
def test_virgin_fetch(self):
|
||||
details = CourseDetails.fetch(self.course_location)
|
||||
self.assertEqual(details.course_location, self.course_location, "Location not copied into")
|
||||
@@ -81,9 +92,9 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
Test the encoder out of its original constrained purpose to see if it functions for general use
|
||||
"""
|
||||
details = {'location': Location(['tag', 'org', 'course', 'category', 'name']),
|
||||
'number': 1,
|
||||
'string': 'string',
|
||||
'datetime': datetime.datetime.now(UTC())}
|
||||
'number': 1,
|
||||
'string': 'string',
|
||||
'datetime': datetime.datetime.now(UTC())}
|
||||
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
||||
jsondetails = json.loads(jsondetails)
|
||||
|
||||
@@ -118,8 +129,60 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
jsondetails.effort, "After set effort"
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_marketing_site_fetch(self):
|
||||
settings_details_url = reverse(
|
||||
'settings_details',
|
||||
kwargs={
|
||||
'org': self.course_location.org,
|
||||
'name': self.course_location.name,
|
||||
'course': self.course_location.course
|
||||
}
|
||||
)
|
||||
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': True}):
|
||||
response = self.client.get(settings_details_url)
|
||||
self.assertContains(response, "Course Summary Page")
|
||||
self.assertContains(response, "course summary page will not be viewable")
|
||||
|
||||
self.assertContains(response, "Course Start Date")
|
||||
self.assertContains(response, "Course End Date")
|
||||
self.assertNotContains(response, "Enrollment Start Date")
|
||||
self.assertNotContains(response, "Enrollment End Date")
|
||||
self.assertContains(response, "not the dates shown on your course summary page")
|
||||
|
||||
self.assertNotContains(response, "Introducing Your Course")
|
||||
self.assertNotContains(response, "Requirements")
|
||||
|
||||
def test_regular_site_fetch(self):
|
||||
settings_details_url = reverse(
|
||||
'settings_details',
|
||||
kwargs={
|
||||
'org': self.course_location.org,
|
||||
'name': self.course_location.name,
|
||||
'course': self.course_location.course
|
||||
}
|
||||
)
|
||||
|
||||
with mock.patch.dict('django.conf.settings.MITX_FEATURES', {'ENABLE_MKTG_SITE': False}):
|
||||
response = self.client.get(settings_details_url)
|
||||
self.assertContains(response, "Course Summary Page")
|
||||
self.assertNotContains(response, "course summary page will not be viewable")
|
||||
|
||||
self.assertContains(response, "Course Start Date")
|
||||
self.assertContains(response, "Course End Date")
|
||||
self.assertContains(response, "Enrollment Start Date")
|
||||
self.assertContains(response, "Enrollment End Date")
|
||||
self.assertNotContains(response, "not the dates shown on your course summary page")
|
||||
|
||||
self.assertContains(response, "Introducing Your Course")
|
||||
self.assertContains(response, "Requirements")
|
||||
|
||||
|
||||
class CourseDetailsViewTest(CourseTestCase):
|
||||
"""
|
||||
Tests for modifying content on the first course settings page (course dates, overview, etc.).
|
||||
"""
|
||||
def alter_field(self, url, details, field, val):
|
||||
setattr(details, field, val)
|
||||
# Need to partially serialize payload b/c the mock doesn't handle it correctly
|
||||
@@ -181,6 +244,9 @@ class CourseDetailsViewTest(CourseTestCase):
|
||||
|
||||
|
||||
class CourseGradingTest(CourseTestCase):
|
||||
"""
|
||||
Tests for the course settings grading page.
|
||||
"""
|
||||
def test_initial_grader(self):
|
||||
descriptor = get_modulestore(self.course_location).get_item(self.course_location)
|
||||
test_grader = CourseGradingModel(descriptor)
|
||||
@@ -256,6 +322,9 @@ class CourseGradingTest(CourseTestCase):
|
||||
|
||||
|
||||
class CourseMetadataEditingTest(CourseTestCase):
|
||||
"""
|
||||
Tests for CourseMetadata.
|
||||
"""
|
||||
def setUp(self):
|
||||
CourseTestCase.setUp(self)
|
||||
# add in the full class too
|
||||
|
||||
@@ -227,7 +227,8 @@ def get_course_settings(request, org, course, name):
|
||||
kwargs={"org": org,
|
||||
"course": course,
|
||||
"name": name,
|
||||
"section": "details"})
|
||||
"section": "details"}),
|
||||
'about_page_editable': not settings.MITX_FEATURES.get('ENABLE_MKTG_SITE', False)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ function removeAsset(e){
|
||||
e.preventDefault();
|
||||
|
||||
var that = this;
|
||||
var msg = new CMS.Models.ConfirmAssetDeleteMessage({
|
||||
var msg = new CMS.Views.Prompt.Confirmation({
|
||||
title: gettext("Delete File Confirmation"),
|
||||
message: gettext("Are you sure you wish to delete this item. It cannot be reversed!\n\nAlso any content that links/refers to this item will no longer work (e.g. broken images and/or links)"),
|
||||
actions: {
|
||||
@@ -17,15 +17,17 @@ function removeAsset(e){
|
||||
text: gettext("OK"),
|
||||
click: function(view) {
|
||||
// call the back-end to actually remove the asset
|
||||
$.post(view.model.get('remove_asset_url'),
|
||||
{ 'location': view.model.get('asset_location') },
|
||||
var url = $('.asset-library').data('remove-asset-callback-url');
|
||||
var row = $(that).closest('tr');
|
||||
$.post(url,
|
||||
{ 'location': row.data('id') },
|
||||
function() {
|
||||
// show the post-commit confirmation
|
||||
$(".wrapper-alert-confirmation").addClass("is-shown").attr('aria-hidden','false');
|
||||
view.model.get('row_to_remove').remove();
|
||||
row.remove();
|
||||
analytics.track('Deleted Asset', {
|
||||
'course': course_location_analytics,
|
||||
'id': view.model.get('asset_location')
|
||||
'id': row.data('id')
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -38,24 +40,9 @@ function removeAsset(e){
|
||||
view.hide();
|
||||
}
|
||||
}]
|
||||
},
|
||||
remove_asset_url: $('.asset-library').data('remove-asset-callback-url'),
|
||||
asset_location: $(this).closest('tr').data('id'),
|
||||
row_to_remove: $(this).closest('tr')
|
||||
}
|
||||
});
|
||||
|
||||
// workaround for now. We can't spawn multiple instances of the Prompt View
|
||||
// so for now, a bit of hackery to just make sure we have a single instance
|
||||
// note: confirm_delete_prompt is in asset_index.html
|
||||
if (confirm_delete_prompt === null)
|
||||
confirm_delete_prompt = new CMS.Views.Prompt({model: msg});
|
||||
else
|
||||
{
|
||||
confirm_delete_prompt.model = msg;
|
||||
confirm_delete_prompt.show();
|
||||
}
|
||||
|
||||
return;
|
||||
return msg.show();
|
||||
}
|
||||
|
||||
function showUploadModal(e) {
|
||||
@@ -125,4 +112,4 @@ function displayFinishedUpload(xhr) {
|
||||
'course': course_location_analytics,
|
||||
'asset_url': resp.url
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ CMS.Views.SystemFeedback = Backbone.View.extend({
|
||||
var parent = CMS.Views[_.str.capitalize(this.options.type)];
|
||||
if(parent && parent.active && parent.active !== this) {
|
||||
parent.active.stopListening();
|
||||
parent.active.undelegateEvents();
|
||||
}
|
||||
this.$el.html(this.template(this.options));
|
||||
parent.active = this;
|
||||
|
||||
@@ -1,2 +1,40 @@
|
||||
// studio - elements - system help
|
||||
// ====================
|
||||
|
||||
// notices - in-context: to be used as notices to users within the context of a form/action
|
||||
.notice-incontext {
|
||||
@extend .ui-well;
|
||||
@include border-radius(($baseline/10));
|
||||
|
||||
.title {
|
||||
@extend .t-title7;
|
||||
margin-bottom: ($baseline/4);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.copy {
|
||||
@extend .t-copy-sub1;
|
||||
@include transition(opacity 0.25s ease-in-out 0);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
.copy {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// particular warnings around a workflow for something
|
||||
.notice-workflow {
|
||||
background: $yellow-l5;
|
||||
|
||||
.copy {
|
||||
color: $gray-d1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ body.course.settings {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@@ -52,6 +52,12 @@ body.course.settings {
|
||||
}
|
||||
}
|
||||
|
||||
// notices - used currently for edx mktg
|
||||
.notice-workflow {
|
||||
margin-top: ($baseline);
|
||||
}
|
||||
|
||||
|
||||
// in form - elements
|
||||
.group-settings {
|
||||
margin: 0 0 ($baseline*2) 0;
|
||||
|
||||
@@ -8,11 +8,6 @@
|
||||
|
||||
<%block name="jsextra">
|
||||
<script src="${static.url('js/vendor/mustache.js')}"></script>
|
||||
|
||||
<script type='text/javascript'>
|
||||
// we just want a singleton
|
||||
confirm_delete_prompt = null;
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
@@ -98,7 +93,7 @@
|
||||
</td>
|
||||
<td class="delete-col">
|
||||
<a href="#" data-tooltip="${_('Delete this asset')}" class="remove-asset-button"><span class="delete-icon"></span></a>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Thank you for signing up for edX edge! To activate your account,
|
||||
Thank you for signing up for edX Studio! To activate your account,
|
||||
please copy and paste this address into your web browser's
|
||||
address bar:
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Schedule & Details Settings</%block>
|
||||
<%block name="bodyclass">is-signedin course schedule settings</%block>
|
||||
@@ -50,8 +52,8 @@ from contentstore import utils
|
||||
<div class="wrapper-mast wrapper">
|
||||
<header class="mast has-subtitle">
|
||||
<h1 class="page-header">
|
||||
<small class="subtitle">Settings</small>
|
||||
<span class="sr">> </span>Schedule & Details
|
||||
<small class="subtitle">${_("Settings")}</small>
|
||||
<span class="sr">> </span>${_("Schedule & Details")}
|
||||
</h1>
|
||||
</header>
|
||||
</div>
|
||||
@@ -62,59 +64,68 @@ from contentstore import utils
|
||||
<form id="settings_details" class="settings-details" method="post" action="">
|
||||
<section class="group-settings basic">
|
||||
<header>
|
||||
<h2 class="title-2">Basic Information</h2>
|
||||
<span class="tip">The nuts and bolts of your course</span>
|
||||
<h2 class="title-2">${_("Basic Information")}</h2>
|
||||
<span class="tip">${_("The nuts and bolts of your course")}</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text is-not-editable" id="field-course-organization">
|
||||
<label for="course-organization">Organization</label>
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
|
||||
<label for="course-organization">${_("Organization")}</label>
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-organization" value="[Course Organization]" readonly />
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-number">
|
||||
<label for="course-number">Course Number</label>
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="short" id="course-number" value="[Course No.]" readonly>
|
||||
<label for="course-number">${_("Course Number")}</label>
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="short" id="course-number" value="[Course No.]" readonly>
|
||||
</li>
|
||||
|
||||
<li class="field text is-not-editable" id="field-course-name">
|
||||
<label for="course-name">Course Name</label>
|
||||
<input title="This field is disabled: this information cannot be changed." type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
<label for="course-name">${_("Course Name")}</label>
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text" class="long" id="course-name" value="[Course Name]" readonly />
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="note note-promotion note-promotion-courseURL has-actions">
|
||||
<h3 class="title">Course Summary Page <span class="tip">(for student enrollment and access)</span></h3>
|
||||
<h3 class="title">${_("Course Summary Page")} <span class="tip">${_("(for student enrollment and access)")}</span></h3>
|
||||
<div class="copy">
|
||||
<p><a class="link-courseURL" rel="external" href="https:${utils.get_lms_link_for_about_page(course_location)}" />https:${utils.get_lms_link_for_about_page(course_location)}</a></p>
|
||||
</div>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action-item">
|
||||
<a title="Send a note to students via email" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i> Invite your students</a>
|
||||
<a title="${_('Send a note to students via email')}" href="mailto:someone@domain.com?Subject=Enroll%20in%20${context_course.display_name_with_default}&body=The%20course%20"${context_course.display_name_with_default}",%20provided%20by%20edX,%20is%20open%20for%20enrollment.%20Please%20navigate%20to%20this%20course%20at%20https:${utils.get_lms_link_for_about_page(course_location)}%20to%20enroll." class="action action-primary"><i class="icon-envelope-alt icon-inline"></i>${_("Invite your students")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
% if not about_page_editable:
|
||||
<div class="notice notice-incontext notice-workflow">
|
||||
<h3 class="title">${_("Promoting Your Course with edX")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('Your course summary page will not be viewable until your course has been announced. To provide content for the page and preview it, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings schedule">
|
||||
<header>
|
||||
<h2 class="title-2">Course Schedule</h2>
|
||||
<span class="tip">Important steps and segments of your course</span>
|
||||
<h2 class="title-2">${_('Course Schedule')}</h2>
|
||||
<span class="tip">${_('Dates that control when your course can be viewed.')}</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-course-start" id="course-start">
|
||||
<div class="field date" id="field-course-start-date">
|
||||
<label for="course-start-date">Course Start Date</label>
|
||||
<label for="course-start-date">${_("Course Start Date")}</label>
|
||||
<input type="text" class="start-date date start datepicker" id="course-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day the course begins</span>
|
||||
<span class="tip tip-stacked">${_("First day the course begins")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-start-time">
|
||||
<label for="course-start-time">Course Start Time</label>
|
||||
<label for="course-start-time">${_("Course Start Time")}</label>
|
||||
<input type="text" class="time start timepicker" id="course-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
@@ -122,29 +133,30 @@ from contentstore import utils
|
||||
|
||||
<li class="field-group field-group-course-end" id="course-end">
|
||||
<div class="field date" id="field-course-end-date">
|
||||
<label for="course-end-date">Course End Date</label>
|
||||
<label for="course-end-date">${_("Course End Date")}</label>
|
||||
<input type="text" class="end-date date end" id="course-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day your course is active</span>
|
||||
<span class="tip tip-stacked">${_("Last day your course is active")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-course-end-time">
|
||||
<label for="course-end-time">Course End Time</label>
|
||||
<label for="course-end-time">${_("Course End Time")}</label>
|
||||
<input type="text" class="time end" id="course-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
% if about_page_editable:
|
||||
<ol class="list-input">
|
||||
<li class="field-group field-group-enrollment-start" id="enrollment-start">
|
||||
<div class="field date" id="field-enrollment-start-date">
|
||||
<label for="course-enrollment-start-date">Enrollment Start Date</label>
|
||||
<label for="course-enrollment-start-date">${_("Enrollment Start Date")}</label>
|
||||
<input type="text" class="start-date date start" id="course-enrollment-start-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">First day students can enroll</span>
|
||||
<span class="tip tip-stacked">${_("First day students can enroll")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-start-time">
|
||||
<label for="course-enrollment-start-time">Enrollment Start Time</label>
|
||||
<label for="course-enrollment-start-time">${_("Enrollment Start Time")}</label>
|
||||
<input type="text" class="time start" id="course-enrollment-start-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
@@ -152,91 +164,106 @@ from contentstore import utils
|
||||
|
||||
<li class="field-group field-group-enrollment-end" id="enrollment-end">
|
||||
<div class="field date" id="field-enrollment-end-date">
|
||||
<label for="course-enrollment-end-date">Enrollment End Date</label>
|
||||
<label for="course-enrollment-end-date">${_("Enrollment End Date")}</label>
|
||||
<input type="text" class="end-date date end" id="course-enrollment-end-date" placeholder="MM/DD/YYYY" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Last day students can enroll</span>
|
||||
<span class="tip tip-stacked">${_("Last day students can enroll")}</span>
|
||||
</div>
|
||||
|
||||
<div class="field time" id="field-enrollment-end-time">
|
||||
<label for="course-enrollment-end-time">Enrollment End Time</label>
|
||||
<label for="course-enrollment-end-time">${_("Enrollment End Time")}</label>
|
||||
<input type="text" class="time end" id="course-enrollment-end-time" value="" placeholder="HH:MM" autocomplete="off" />
|
||||
<span class="tip tip-stacked" id="timezone"></span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
% if not about_page_editable:
|
||||
<div class="notice notice-incontext notice-workflow">
|
||||
<h3 class="title">${_("These Dates Are Not Used When Promoting Your Course")}</h3>
|
||||
<div class="copy">
|
||||
<p>${_('These dates impact <strong>when your courseware can be viewed</strong>, but they are <strong>not the dates shown on your course summary page</strong>. To provide the course start and registration dates as shown on your course summary page, follow the instructions provided by your <abbr title="Program Manager">PM</abbr> or Conrad Warre <a rel="email" class="action action-email" href="mailto:conrad@edx.org">(conrad@edx.org)</a>.')}</p>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
<hr class="divide" />
|
||||
% if about_page_editable:
|
||||
<section class="group-settings marketing">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Introducing Your Course")}</h2>
|
||||
<span class="tip">${_("Information for prospective students")}</span>
|
||||
</header>
|
||||
|
||||
<section class="group-settings marketing">
|
||||
<header>
|
||||
<h2 class="title-2">Introducing Your Course</h2>
|
||||
<span class="tip">Information for prospective students</span>
|
||||
</header>
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-overview">
|
||||
<label for="course-overview">${_("Course Overview")}</label>
|
||||
<textarea class="tinymce text-editor" id="course-overview"></textarea>
|
||||
<%def name='overview_text()'><%
|
||||
a_link_start = '<a class="link-courseURL" rel="external" href="'
|
||||
a_link_end = '">' + _("your course summary page") + '</a>'
|
||||
a_link = a_link_start + utils.get_lms_link_for_about_page(course_location) + a_link_end
|
||||
text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link
|
||||
%>${text}</%def>
|
||||
<span class="tip tip-stacked">${overview_text()}</span>
|
||||
</li>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-overview">
|
||||
<label for="course-overview">Course Overview</label>
|
||||
<textarea class="tinymce text-editor" id="course-overview"></textarea>
|
||||
<span class="tip tip-stacked">Introductions, prerequisites, FAQs that are used on <a class="link-courseURL" rel="external" href="${utils.get_lms_link_for_about_page(course_location)}">your course summary page</a> (formatted in HTML)</span>
|
||||
</li>
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
<label for="course-overview">${_("Course Introduction Video")}</label>
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span>${_("Delete Current Video")}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<li class="field video" id="field-course-introduction-video">
|
||||
<label for="course-overview">Course Introduction Video</label>
|
||||
<div class="input input-existing">
|
||||
<div class="current current-course-introduction-video">
|
||||
<iframe width="618" height="350" src="" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#" class="remove-item remove-course-introduction-video remove-video-data"><span class="delete-icon"></span> Delete Current Video</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
|
||||
<span class="tip tip-stacked">${_("Enter your YouTube video's ID (along with any restriction parameters)")}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<div class="input">
|
||||
<input type="text" class="long new-course-introduction-video add-video-data" id="course-introduction-video" value="" placeholder="your YouTube video's ID" autocomplete="off" />
|
||||
<span class="tip tip-stacked">Enter your YouTube video's ID (along with any restriction parameters)</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<hr class="divide" />
|
||||
|
||||
<hr class="divide" />
|
||||
<section class="group-settings requirements">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Requirements")}</h2>
|
||||
<span class="tip">${_("Expectations of the students taking this course")}</span>
|
||||
</header>
|
||||
|
||||
<section class="group-settings requirements">
|
||||
<header>
|
||||
<h2 class="title-2">Requirements</h2>
|
||||
<span class="tip">Expectations of the students taking this course</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-effort">
|
||||
<label for="course-effort">Hours of Effort per Week</label>
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
|
||||
<span class="tip tip-inline">Time spent on all course work</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-effort">
|
||||
<label for="course-effort">${_("Hours of Effort per Week")}</label>
|
||||
<input type="text" class="short time" id="course-effort" placeholder="HH:MM" />
|
||||
<span class="tip tip-inline">${_("Time spent on all course work")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
% endif
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">How will these settings be used?</h3>
|
||||
<p>Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.</p>
|
||||
<h3 class="title-3">${_("How will these settings be used?")}</h3>
|
||||
<p>${_("Your course's schedule settings determine when students can enroll in and begin a course.")}</p>
|
||||
|
||||
<p>Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.</p>
|
||||
<p>${_("Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.")}</p>
|
||||
</div>
|
||||
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
<% ctx_loc = context_course.location %>
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<h3 class="title-3">Other Course Settings</h3>
|
||||
<h3 class="title-3">${_("Other Course Settings")}</h3>
|
||||
<nav class="nav-related">
|
||||
<ul>
|
||||
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Grading</a></li>
|
||||
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">Course Team</a></li>
|
||||
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">Advanced Settings</a></li>
|
||||
<li class="nav-item"><a href="${reverse('contentstore.views.course_config_graders_page', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Grading")}</a></li>
|
||||
<li class="nav-item"><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}">${_("Course Team")}</a></li>
|
||||
<li class="nav-item"><a href="${reverse('course_advanced_settings', kwargs={'org' : ctx_loc.org, 'course' : ctx_loc.course, 'name': ctx_loc.name})}">${_("Advanced Settings")}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
% endif
|
||||
|
||||
@@ -49,7 +49,7 @@ def css_has_text(css_selector, text):
|
||||
|
||||
@world.absorb
|
||||
def css_find(css, wait_time=5):
|
||||
def is_visible(driver):
|
||||
def is_visible(_driver):
|
||||
return EC.visibility_of_element_located((By.CSS_SELECTOR, css,))
|
||||
|
||||
world.browser.is_element_present_by_css(css, wait_time=wait_time)
|
||||
@@ -58,19 +58,26 @@ def css_find(css, wait_time=5):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_click(css_selector, index=0, attempts=5):
|
||||
def css_click(css_selector, index=0, max_attempts=5, success_condition=lambda: True):
|
||||
"""
|
||||
Perform a click on a CSS selector, retrying if it initially fails
|
||||
This function will return if the click worked (since it is try/excepting all errors)
|
||||
Perform a click on a CSS selector, retrying if it initially fails.
|
||||
|
||||
This function handles errors that may be thrown if the component cannot be clicked on.
|
||||
However, there are cases where an error may not be thrown, and yet the operation did not
|
||||
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the click worked.
|
||||
|
||||
This function will return True if the click worked (taking into account both errors and the optional
|
||||
success_condition).
|
||||
"""
|
||||
assert is_css_present(css_selector)
|
||||
attempt = 0
|
||||
result = False
|
||||
while attempt < attempts:
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
world.css_find(css_selector)[index].click()
|
||||
result = True
|
||||
break
|
||||
if success_condition():
|
||||
result = True
|
||||
break
|
||||
except WebDriverException:
|
||||
# Occasionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
@@ -83,15 +90,47 @@ def css_click(css_selector, index=0, attempts=5):
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_click_at(css, x=10, y=10):
|
||||
def css_check(css_selector, index=0, max_attempts=5, success_condition=lambda: True):
|
||||
"""
|
||||
Checks a check box based on a CSS selector, retrying if it initially fails.
|
||||
|
||||
This function handles errors that may be thrown if the component cannot be clicked on.
|
||||
However, there are cases where an error may not be thrown, and yet the operation did not
|
||||
actually succeed. For those cases, a success_condition lambda can be supplied to verify that the check worked.
|
||||
|
||||
This function will return True if the check worked (taking into account both errors and the optional
|
||||
success_condition).
|
||||
"""
|
||||
assert is_css_present(css_selector)
|
||||
attempt = 0
|
||||
result = False
|
||||
while attempt < max_attempts:
|
||||
try:
|
||||
world.css_find(css_selector)[index].check()
|
||||
if success_condition():
|
||||
result = True
|
||||
break
|
||||
except WebDriverException:
|
||||
# Occasionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
# If this happens, wait a second, then try again
|
||||
world.wait(1)
|
||||
attempt += 1
|
||||
except:
|
||||
attempt += 1
|
||||
return result
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_click_at(css, x_cord=10, y_cord=10):
|
||||
'''
|
||||
A method to click at x,y coordinates of the element
|
||||
rather than in the center of the element
|
||||
'''
|
||||
e = css_find(css).first
|
||||
e.action_chains.move_to_element_with_offset(e._element, x, y)
|
||||
e.action_chains.click()
|
||||
e.action_chains.perform()
|
||||
element = css_find(css).first
|
||||
element.action_chains.move_to_element_with_offset(element._element, x_cord, y_cord)
|
||||
element.action_chains.click()
|
||||
element.action_chains.perform()
|
||||
|
||||
|
||||
@world.absorb
|
||||
@@ -136,7 +175,7 @@ def css_visible(css_selector):
|
||||
|
||||
@world.absorb
|
||||
def dialogs_closed():
|
||||
def are_dialogs_closed(driver):
|
||||
def are_dialogs_closed(_driver):
|
||||
'''
|
||||
Return True when no modal dialogs are visible
|
||||
'''
|
||||
@@ -147,12 +186,12 @@ def dialogs_closed():
|
||||
|
||||
@world.absorb
|
||||
def save_the_html(path='/tmp'):
|
||||
u = world.browser.url
|
||||
url = world.browser.url
|
||||
html = world.browser.html.encode('ascii', 'ignore')
|
||||
filename = '%s.html' % quote_plus(u)
|
||||
f = open('%s/%s' % (path, filename), 'w')
|
||||
f.write(html)
|
||||
f.close()
|
||||
filename = '%s.html' % quote_plus(url)
|
||||
file = open('%s/%s' % (path, filename), 'w')
|
||||
file.write(html)
|
||||
file.close()
|
||||
|
||||
|
||||
@world.absorb
|
||||
|
||||
@@ -12,8 +12,8 @@ from path import path
|
||||
from cStringIO import StringIO
|
||||
from collections import defaultdict
|
||||
|
||||
from .calc import UndefinedVariable
|
||||
from .capa_problem import LoncapaProblem
|
||||
from calc import UndefinedVariable
|
||||
from capa.capa_problem import LoncapaProblem
|
||||
from mako.lookup import TemplateLookup
|
||||
|
||||
logging.basicConfig(format="%(levelname)s %(message)s")
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
"""
|
||||
Tests to verify that CorrectMap behaves correctly
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from capa.correctmap import CorrectMap
|
||||
import datetime
|
||||
|
||||
|
||||
class CorrectMapTest(unittest.TestCase):
|
||||
"""
|
||||
Tests to verify that CorrectMap behaves correctly
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.cmap = CorrectMap()
|
||||
|
||||
def test_set_input_properties(self):
|
||||
|
||||
# Set the correctmap properties for two inputs
|
||||
self.cmap.set(answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={'key':'secretstring',
|
||||
'time':'20130228100026'})
|
||||
self.cmap.set(
|
||||
answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={
|
||||
'key': 'secretstring',
|
||||
'time': '20130228100026'
|
||||
}
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='2_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None,
|
||||
msg=None,
|
||||
hint=None,
|
||||
hintmode=None,
|
||||
queuestate=None)
|
||||
self.cmap.set(
|
||||
answer_id='2_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None,
|
||||
msg=None,
|
||||
hint=None,
|
||||
hintmode=None,
|
||||
queuestate=None
|
||||
)
|
||||
|
||||
# Assert that each input has the expected properties
|
||||
self.assertTrue(self.cmap.is_correct('1_2_1'))
|
||||
@@ -62,7 +75,6 @@ class CorrectMapTest(unittest.TestCase):
|
||||
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', ''))
|
||||
self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None))
|
||||
|
||||
|
||||
def test_get_npoints(self):
|
||||
# Set the correctmap properties for 4 inputs
|
||||
# 1) correct, 5 points
|
||||
@@ -70,25 +82,35 @@ class CorrectMapTest(unittest.TestCase):
|
||||
# 3) incorrect, 5 points
|
||||
# 4) incorrect, None points
|
||||
# 5) correct, 0 points
|
||||
self.cmap.set(answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5)
|
||||
self.cmap.set(
|
||||
answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='2_2_1',
|
||||
correctness='correct',
|
||||
npoints=None)
|
||||
self.cmap.set(
|
||||
answer_id='2_2_1',
|
||||
correctness='correct',
|
||||
npoints=None
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='3_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=5)
|
||||
self.cmap.set(
|
||||
answer_id='3_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=5
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='4_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None)
|
||||
self.cmap.set(
|
||||
answer_id='4_2_1',
|
||||
correctness='incorrect',
|
||||
npoints=None
|
||||
)
|
||||
|
||||
self.cmap.set(answer_id='5_2_1',
|
||||
correctness='correct',
|
||||
npoints=0)
|
||||
self.cmap.set(
|
||||
answer_id='5_2_1',
|
||||
correctness='correct',
|
||||
npoints=0
|
||||
)
|
||||
|
||||
# Assert that we get the expected points
|
||||
# If points assigned --> npoints
|
||||
@@ -100,7 +122,6 @@ class CorrectMapTest(unittest.TestCase):
|
||||
self.assertEqual(self.cmap.get_npoints('4_2_1'), 0)
|
||||
self.assertEqual(self.cmap.get_npoints('5_2_1'), 0)
|
||||
|
||||
|
||||
def test_set_overall_message(self):
|
||||
|
||||
# Default is an empty string string
|
||||
@@ -118,14 +139,18 @@ class CorrectMapTest(unittest.TestCase):
|
||||
|
||||
def test_update_from_correctmap(self):
|
||||
# Initialize a CorrectMap with some properties
|
||||
self.cmap.set(answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={'key':'secretstring',
|
||||
'time':'20130228100026'})
|
||||
self.cmap.set(
|
||||
answer_id='1_2_1',
|
||||
correctness='correct',
|
||||
npoints=5,
|
||||
msg='Test message',
|
||||
hint='Test hint',
|
||||
hintmode='always',
|
||||
queuestate={
|
||||
'key': 'secretstring',
|
||||
'time': '20130228100026'
|
||||
}
|
||||
)
|
||||
|
||||
self.cmap.set_overall_message("Test message")
|
||||
|
||||
@@ -133,14 +158,17 @@ class CorrectMapTest(unittest.TestCase):
|
||||
# as the first cmap
|
||||
other_cmap = CorrectMap()
|
||||
other_cmap.update(self.cmap)
|
||||
|
||||
|
||||
# Assert that it has all the same properties
|
||||
self.assertEqual(other_cmap.get_overall_message(),
|
||||
self.cmap.get_overall_message())
|
||||
|
||||
self.assertEqual(other_cmap.get_dict(),
|
||||
self.cmap.get_dict())
|
||||
self.assertEqual(
|
||||
other_cmap.get_overall_message(),
|
||||
self.cmap.get_overall_message()
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
other_cmap.get_dict(),
|
||||
self.cmap.get_dict()
|
||||
)
|
||||
|
||||
def test_update_from_invalid(self):
|
||||
# Should get an exception if we try to update() a CorrectMap
|
||||
|
||||
@@ -279,7 +279,7 @@ class CapaModule(CapaFields, XModule):
|
||||
"""
|
||||
Return True/False to indicate whether to show the "Check" button.
|
||||
"""
|
||||
submitted_without_reset = (self.is_completed() and self.rerandomize == "always")
|
||||
submitted_without_reset = (self.is_submitted() and self.rerandomize == "always")
|
||||
|
||||
# If the problem is closed (past due / too many attempts)
|
||||
# then we do NOT show the "check" button
|
||||
@@ -302,7 +302,7 @@ class CapaModule(CapaFields, XModule):
|
||||
# then do NOT show the reset button.
|
||||
# If the problem hasn't been submitted yet, then do NOT show
|
||||
# the reset button.
|
||||
if (self.closed() and not is_survey_question) or not self.is_completed():
|
||||
if (self.closed() and not is_survey_question) or not self.is_submitted():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -322,7 +322,7 @@ class CapaModule(CapaFields, XModule):
|
||||
return not self.closed()
|
||||
else:
|
||||
is_survey_question = (self.max_attempts == 0)
|
||||
needs_reset = self.is_completed() and self.rerandomize == "always"
|
||||
needs_reset = self.is_submitted() and self.rerandomize == "always"
|
||||
|
||||
# If the student has unlimited attempts, and their answers
|
||||
# are not randomized, then we do not need a save button
|
||||
@@ -516,13 +516,18 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
return False
|
||||
|
||||
def is_completed(self):
|
||||
# used by conditional module
|
||||
# return self.answer_available()
|
||||
def is_submitted(self):
|
||||
"""
|
||||
Used to decide to show or hide RESET or CHECK buttons.
|
||||
|
||||
Means that student submitted problem and nothing more.
|
||||
Problem can be completely wrong.
|
||||
Pressing RESET button makes this function to return False.
|
||||
"""
|
||||
return self.lcp.done
|
||||
|
||||
def is_attempted(self):
|
||||
# used by conditional module
|
||||
"""Used by conditional module"""
|
||||
return self.attempts > 0
|
||||
|
||||
def is_correct(self):
|
||||
|
||||
@@ -35,8 +35,11 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
<conditional> tag attributes:
|
||||
sources - location id of required modules, separated by ';'
|
||||
|
||||
completed - map to `is_completed` module method
|
||||
submitted - map to `is_submitted` module method.
|
||||
(pressing RESET button makes this function to return False.)
|
||||
|
||||
attempted - map to `is_attempted` module method
|
||||
correct - map to `is_correct` module method
|
||||
poll_answer - map to `poll_answer` module attribute
|
||||
voted - map to `voted` module attribute
|
||||
|
||||
@@ -70,8 +73,18 @@ class ConditionalModule(ConditionalFields, XModule):
|
||||
# value: <name of module attribute>
|
||||
conditions_map = {
|
||||
'poll_answer': 'poll_answer', # poll_question attr
|
||||
'completed': 'is_completed', # capa_problem attr
|
||||
|
||||
# problem was submitted (it can be wrong)
|
||||
# if student will press reset button after that,
|
||||
# state will be reverted
|
||||
'submitted': 'is_submitted', # capa_problem attr
|
||||
|
||||
# if student attempted problem
|
||||
'attempted': 'is_attempted', # capa_problem attr
|
||||
|
||||
# if problem is full points
|
||||
'correct': 'is_correct',
|
||||
|
||||
'voted': 'voted' # poll_question attr
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<div id="video_example">
|
||||
<div id="example">
|
||||
<div id="video_id" class="video"
|
||||
data-streams="0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId"
|
||||
data-youtube-id-0-75="slowerSpeedYoutubeId"
|
||||
data-youtube-id-1-0="normalSpeedYoutubeId"
|
||||
data-show-captions="true"
|
||||
data-start=""
|
||||
data-end=""
|
||||
@@ -18,4 +19,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ describe 'Video', ->
|
||||
loadFixtures 'video.html'
|
||||
jasmine.stubRequests()
|
||||
|
||||
@videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId'
|
||||
@slowerSpeedYoutubeId = 'slowerSpeedYoutubeId'
|
||||
@normalSpeedYoutubeId = 'normalSpeedYoutubeId'
|
||||
metadata =
|
||||
@@ -30,7 +29,7 @@ describe 'Video', ->
|
||||
beforeEach ->
|
||||
spyOn(window.Video.prototype, 'fetchMetadata').andCallFake ->
|
||||
@metadata = metadata
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
it 'reset the current video player', ->
|
||||
expect(window.player).toBeNull()
|
||||
|
||||
@@ -60,7 +59,7 @@ describe 'Video', ->
|
||||
@originalYT = window.YT
|
||||
window.YT = { Player: true }
|
||||
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
|
||||
afterEach ->
|
||||
window.YT = @originalYT
|
||||
@@ -73,7 +72,7 @@ describe 'Video', ->
|
||||
beforeEach ->
|
||||
@originalYT = window.YT
|
||||
window.YT = {}
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
|
||||
afterEach ->
|
||||
window.YT = @originalYT
|
||||
@@ -86,7 +85,7 @@ describe 'Video', ->
|
||||
@originalYT = window.YT
|
||||
window.YT = {}
|
||||
spyOn(window, 'VideoPlayer').andReturn(@stubVideoPlayer)
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
window.onYouTubePlayerAPIReady()
|
||||
|
||||
afterEach ->
|
||||
@@ -99,7 +98,7 @@ describe 'Video', ->
|
||||
describe 'youtubeId', ->
|
||||
beforeEach ->
|
||||
$.cookie.andReturn '1.0'
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
|
||||
describe 'with speed', ->
|
||||
it 'return the video id for given speed', ->
|
||||
@@ -112,7 +111,7 @@ describe 'Video', ->
|
||||
|
||||
describe 'setSpeed', ->
|
||||
beforeEach ->
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
|
||||
describe 'when new speed is available', ->
|
||||
beforeEach ->
|
||||
@@ -133,14 +132,14 @@ describe 'Video', ->
|
||||
|
||||
describe 'getDuration', ->
|
||||
beforeEach ->
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
|
||||
it 'return duration for current video', ->
|
||||
expect(@video.getDuration()).toEqual 200
|
||||
|
||||
describe 'log', ->
|
||||
beforeEach ->
|
||||
@video = new Video '#example', @videosDefinition
|
||||
@video = new Video '#example'
|
||||
@video.setSpeed '1.0'
|
||||
spyOn Logger, 'log'
|
||||
@video.player = { currentTime: 25 }
|
||||
|
||||
@@ -8,7 +8,7 @@ class @Video
|
||||
@show_captions = @el.data('show-captions')
|
||||
window.player = null
|
||||
@el = $("#video_#{@id}")
|
||||
@parseVideos @el.data('streams')
|
||||
@parseVideos()
|
||||
@fetchMetadata()
|
||||
@parseSpeed()
|
||||
$("#video_#{@id}").data('video', this).addClass('video-load-complete')
|
||||
@@ -27,10 +27,14 @@ class @Video
|
||||
|
||||
parseVideos: (videos) ->
|
||||
@videos = {}
|
||||
$.each videos.split(/,/), (index, video) =>
|
||||
video = video.split(/:/)
|
||||
speed = parseFloat(video[0]).toFixed(2).replace /\.00$/, '.0'
|
||||
@videos[speed] = video[1]
|
||||
if @el.data('youtube-id-0-75')
|
||||
@videos['0.75'] = @el.data('youtube-id-0-75')
|
||||
if @el.data('youtube-id-1-0')
|
||||
@videos['1.0'] = @el.data('youtube-id-1-0')
|
||||
if @el.data('youtube-id-1-25')
|
||||
@videos['1.25'] = @el.data('youtube-id-1-25')
|
||||
if @el.data('youtube-id-1-5')
|
||||
@videos['1.50'] = @el.data('youtube-id-1-5')
|
||||
|
||||
parseSpeed: ->
|
||||
@setSpeed($.cookie('video_speed'))
|
||||
|
||||
@@ -4,6 +4,7 @@ This module has utility functions for gathering up the static content
|
||||
that is defined by XModules and XModuleDescriptors (javascript and css)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
import os
|
||||
import errno
|
||||
@@ -15,6 +16,9 @@ from path import path
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def write_module_styles(output_root):
|
||||
return _write_styles('.xmodule_display', output_root, _list_modules())
|
||||
|
||||
@@ -121,18 +125,32 @@ def _write_js(output_root, classes):
|
||||
type=filetype)
|
||||
contents[filename] = fragment
|
||||
|
||||
_write_files(output_root, contents)
|
||||
_write_files(output_root, contents, {'.coffee': '.js'})
|
||||
|
||||
return [output_root / filename for filename in contents.keys()]
|
||||
|
||||
|
||||
def _write_files(output_root, contents):
|
||||
def _write_files(output_root, contents, generated_suffix_map=None):
|
||||
_ensure_dir(output_root)
|
||||
for extra_file in set(output_root.files()) - set(contents.keys()):
|
||||
extra_file.remove_p()
|
||||
to_delete = set(file.basename() for file in output_root.files()) - set(contents.keys())
|
||||
|
||||
if generated_suffix_map:
|
||||
for output_file in contents.keys():
|
||||
for suffix, generated_suffix in generated_suffix_map.items():
|
||||
if output_file.endswith(suffix):
|
||||
to_delete.discard(output_file.replace(suffix, generated_suffix))
|
||||
|
||||
for extra_file in to_delete:
|
||||
(output_root / extra_file).remove_p()
|
||||
|
||||
for filename, file_content in contents.iteritems():
|
||||
(output_root / filename).write_bytes(file_content)
|
||||
output_file = output_root / filename
|
||||
|
||||
if not output_file.isfile() or output_file.read_md5() != hashlib.md5(file_content).digest():
|
||||
LOG.debug("Writing %s", output_file)
|
||||
output_file.write_bytes(file_content)
|
||||
else:
|
||||
LOG.debug("%s unchanged, skipping", output_file)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: default
|
||||
data_dir: a_made_up_name
|
||||
data: |
|
||||
<video youtube="0.75:JMD_ifUUfsU,1.0:OEoXaMPEzfM,1.25:AKqURZnYqpk,1.50:DYpADpL7jAY"/>
|
||||
data: ""
|
||||
children: []
|
||||
|
||||
@@ -29,14 +29,14 @@ open_ended_grading_interface = {
|
||||
}
|
||||
|
||||
|
||||
def test_system():
|
||||
def get_test_system():
|
||||
"""
|
||||
Construct a test ModuleSystem instance.
|
||||
|
||||
By default, the render_template() method simply returns the repr of the
|
||||
context it is passed. You can override this behavior by monkey patching::
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = my_render_func
|
||||
|
||||
where `my_render_func` is a function of the form my_render_func(template, context).
|
||||
|
||||
@@ -8,7 +8,7 @@ from mock import Mock
|
||||
from xmodule.annotatable_module import AnnotatableModule
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
class AnnotatableModuleTestCase(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "toy", "annotatable", "guided_discussion"])
|
||||
@@ -32,7 +32,7 @@ class AnnotatableModuleTestCase(unittest.TestCase):
|
||||
module_data = {'data': sample_xml, 'location': location}
|
||||
|
||||
def setUp(self):
|
||||
self.annotatable = AnnotatableModule(test_system(), self.descriptor, self.module_data)
|
||||
self.annotatable = AnnotatableModule(get_test_system(), self.descriptor, self.module_data)
|
||||
|
||||
def test_annotation_data_attr(self):
|
||||
el = etree.fromstring('<annotation title="bar" body="foo" problem="0">test</annotation>')
|
||||
|
||||
@@ -17,7 +17,7 @@ from xmodule.modulestore import Location
|
||||
|
||||
from django.http import QueryDict
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
from pytz import UTC
|
||||
from capa.correctmap import CorrectMap
|
||||
|
||||
@@ -112,7 +112,7 @@ class CapaFactory(object):
|
||||
# since everything else is a string.
|
||||
model_data['attempts'] = int(attempts)
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
module = CapaModule(system, descriptor, model_data)
|
||||
|
||||
@@ -1002,7 +1002,7 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# is asked to render itself as HTML
|
||||
module.lcp.get_html = Mock(side_effect=Exception("Test"))
|
||||
|
||||
# Stub out the test_system rendering function
|
||||
# Stub out the get_test_system rendering function
|
||||
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
|
||||
# Turn off DEBUG
|
||||
|
||||
@@ -18,7 +18,7 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
ORG = 'edX'
|
||||
COURSE = 'open_ended' # name of directory with course data
|
||||
@@ -68,7 +68,7 @@ class OpenEndedChildTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.openendedchild = OpenEndedChild(self.test_system, self.location,
|
||||
self.definition, self.descriptor, self.static_data, self.metadata)
|
||||
|
||||
@@ -192,7 +192,7 @@ class OpenEndedModuleTest(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
self.test_system.location = self.location
|
||||
self.mock_xqueue = MagicMock()
|
||||
@@ -367,7 +367,7 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
|
||||
definition = {'prompt': etree.XML(prompt), 'rubric': etree.XML(rubric), 'task_xml': [task_xml1, task_xml2]}
|
||||
full_definition = definition_template.format(prompt=prompt, rubric=rubric, task1=task_xml1, task2=task_xml2)
|
||||
descriptor = Mock(data=full_definition)
|
||||
test_system = test_system()
|
||||
test_system = get_test_system()
|
||||
combinedoe_container = CombinedOpenEndedModule(
|
||||
test_system,
|
||||
descriptor,
|
||||
@@ -493,7 +493,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
hint = "blah"
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.xqueue['interface'] = Mock(
|
||||
send_to_queue=Mock(side_effect=[1, "queued"])
|
||||
)
|
||||
@@ -569,6 +569,7 @@ class OpenEndedModuleXmlTest(unittest.TestCase, DummyModulestore):
|
||||
#Mock a student submitting an assessment
|
||||
assessment_dict = MockQueryDict()
|
||||
assessment_dict.update({'assessment': sum(assessment), 'score_list[]': assessment})
|
||||
#from nose.tools import set_trace; set_trace()
|
||||
module.handle_ajax("save_assessment", assessment_dict)
|
||||
task_one_json = json.loads(module.task_states[0])
|
||||
self.assertEqual(json.loads(task_one_json['child_history'][0]['post_assessment']), assessment)
|
||||
|
||||
@@ -15,7 +15,7 @@ from xmodule.tests.test_export import DATA_DIR
|
||||
ORG = 'test_org'
|
||||
COURSE = 'conditional' # name of directory with course data
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
|
||||
class DummySystem(ImportSystem):
|
||||
@@ -104,7 +104,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
def test_icon_class(self):
|
||||
'''verify that get_icon_class works independent of condition satisfaction'''
|
||||
@@ -117,7 +117,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
|
||||
def test_get_html(self):
|
||||
modules = ConditionalFactory.create(self.test_system)
|
||||
# because test_system returns the repr of the context dict passed to render_template,
|
||||
# because get_test_system returns the repr of the context dict passed to render_template,
|
||||
# we reverse it here
|
||||
html = modules['cond_module'].get_html()
|
||||
html_dict = literal_eval(html)
|
||||
@@ -161,7 +161,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
return DummySystem(load_error_modules)
|
||||
|
||||
def setUp(self):
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
def get_course(self, name):
|
||||
"""Get a test course by directory name. If there's more than one, error."""
|
||||
|
||||
@@ -2,25 +2,30 @@
|
||||
Tests for ErrorModule and NonStaffErrorModule
|
||||
"""
|
||||
import unittest
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
import xmodule.error_module as error_module
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from mock import MagicMock
|
||||
|
||||
|
||||
class TestErrorModule(unittest.TestCase):
|
||||
"""
|
||||
Tests for ErrorModule and ErrorDescriptor
|
||||
"""
|
||||
class SetupTestErrorModules():
|
||||
def setUp(self):
|
||||
self.system = test_system()
|
||||
self.system = get_test_system()
|
||||
self.org = "org"
|
||||
self.course = "course"
|
||||
self.location = Location(['i4x', self.org, self.course, None, None])
|
||||
self.valid_xml = u"<problem>ABC \N{SNOWMAN}</problem>"
|
||||
self.error_msg = "Error"
|
||||
|
||||
|
||||
class TestErrorModule(unittest.TestCase, SetupTestErrorModules):
|
||||
"""
|
||||
Tests for ErrorModule and ErrorDescriptor
|
||||
"""
|
||||
def setUp(self):
|
||||
SetupTestErrorModules.setUp(self)
|
||||
|
||||
def test_error_module_xml_rendering(self):
|
||||
descriptor = error_module.ErrorDescriptor.from_xml(
|
||||
self.valid_xml, self.system, self.org, self.course, self.error_msg)
|
||||
@@ -45,10 +50,12 @@ class TestErrorModule(unittest.TestCase):
|
||||
self.assertIn(repr(descriptor), context_repr)
|
||||
|
||||
|
||||
class TestNonStaffErrorModule(TestErrorModule):
|
||||
class TestNonStaffErrorModule(unittest.TestCase, SetupTestErrorModules):
|
||||
"""
|
||||
Tests for NonStaffErrorModule and NonStaffErrorDescriptor
|
||||
"""
|
||||
def setUp(self):
|
||||
SetupTestErrorModules.setUp(self)
|
||||
|
||||
def test_non_staff_error_module_create(self):
|
||||
descriptor = error_module.NonStaffErrorDescriptor.from_xml(
|
||||
|
||||
@@ -5,7 +5,7 @@ from mock import Mock
|
||||
from xmodule.html_module import HtmlModule
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
class HtmlModuleSubstitutionTestCase(unittest.TestCase):
|
||||
descriptor = Mock()
|
||||
@@ -13,7 +13,7 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
|
||||
def test_substitution_works(self):
|
||||
sample_xml = '''%%USER_ID%%'''
|
||||
module_data = {'data': sample_xml}
|
||||
module_system = test_system()
|
||||
module_system = get_test_system()
|
||||
module = HtmlModule(module_system, self.descriptor, module_data)
|
||||
self.assertEqual(module.get_html(), str(module_system.anonymous_student_id))
|
||||
|
||||
@@ -25,14 +25,14 @@ class HtmlModuleSubstitutionTestCase(unittest.TestCase):
|
||||
</html>
|
||||
'''
|
||||
module_data = {'data': sample_xml}
|
||||
module = HtmlModule(test_system(), self.descriptor, module_data)
|
||||
module = HtmlModule(get_test_system(), self.descriptor, module_data)
|
||||
self.assertEqual(module.get_html(), sample_xml)
|
||||
|
||||
|
||||
def test_substitution_without_anonymous_student_id(self):
|
||||
sample_xml = '''%%USER_ID%%'''
|
||||
module_data = {'data': sample_xml}
|
||||
module_system = test_system()
|
||||
module_system = get_test_system()
|
||||
module_system.anonymous_student_id = None
|
||||
module = HtmlModule(module_system, self.descriptor, module_data)
|
||||
self.assertEqual(module.get_html(), sample_xml)
|
||||
|
||||
@@ -336,8 +336,8 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
location = Location(["i4x", "edX", "toy", "video", "Welcome"])
|
||||
toy_video = modulestore.get_instance(toy_id, location)
|
||||
two_toy_video = modulestore.get_instance(two_toy_id, location)
|
||||
self.assertEqual(etree.fromstring(toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh8")
|
||||
self.assertEqual(etree.fromstring(two_toy_video.data).get('youtube'), "1.0:p2Q6BrNhdh9")
|
||||
self.assertEqual(toy_video.youtube_id_1_0, "p2Q6BrNhdh8")
|
||||
self.assertEqual(two_toy_video.youtube_id_1_0, "p2Q6BrNhdh9")
|
||||
|
||||
def test_colon_in_url_name(self):
|
||||
"""Ensure that colons in url_names convert to file paths properly"""
|
||||
|
||||
@@ -8,7 +8,7 @@ import unittest
|
||||
from xmodule.poll_module import PollDescriptor
|
||||
from xmodule.conditional_module import ConditionalDescriptor
|
||||
from xmodule.word_cloud_module import WordCloudDescriptor
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
|
||||
class PostData:
|
||||
"""Class which emulate postdata."""
|
||||
@@ -30,7 +30,7 @@ class LogicTest(unittest.TestCase):
|
||||
"""Empty object."""
|
||||
pass
|
||||
|
||||
self.system = test_system()
|
||||
self.system = get_test_system()
|
||||
self.descriptor = EmptyClass()
|
||||
|
||||
self.xmodule_class = self.descriptor_class.module_class
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
from xmodule.modulestore import Location
|
||||
from .import test_system
|
||||
from .import get_test_system
|
||||
from test_util_open_ended import MockQueryDict, DummyModulestore
|
||||
import json
|
||||
|
||||
@@ -39,7 +39,7 @@ class PeerGradingModuleTest(unittest.TestCase, DummyModulestore):
|
||||
Create a peer grading module from a test system
|
||||
@return:
|
||||
"""
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.setup_modulestore(COURSE)
|
||||
self.peer_grading = self.get_module_from_location(self.problem_location, COURSE)
|
||||
@@ -151,10 +151,10 @@ class PeerGradingModuleScoredTest(unittest.TestCase, DummyModulestore):
|
||||
Create a peer grading module from a test system
|
||||
@return:
|
||||
"""
|
||||
self.test_system = test_system()
|
||||
self.test_system = get_test_system()
|
||||
self.test_system.open_ended_grading_interface = None
|
||||
self.setup_modulestore(COURSE)
|
||||
|
||||
def test_metadata_load(self):
|
||||
peer_grading = self.get_module_from_location(self.problem_location, COURSE)
|
||||
self.assertEqual(peer_grading.closed(), False)
|
||||
self.assertEqual(peer_grading.closed(), False)
|
||||
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
from xmodule.progress import Progress
|
||||
from xmodule import x_module
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
@@ -134,6 +134,6 @@ class ModuleProgressTest(unittest.TestCase):
|
||||
'''
|
||||
def test_xmodule_default(self):
|
||||
'''Make sure default get_progress exists, returns None'''
|
||||
xm = x_module.XModule(test_system(), None, {'location': 'a://b/c/d/e'})
|
||||
xm = x_module.XModule(get_test_system(), None, {'location': 'a://b/c/d/e'})
|
||||
p = xm.get_progress()
|
||||
self.assertEqual(p, None)
|
||||
|
||||
@@ -6,7 +6,7 @@ from xmodule.open_ended_grading_classes.self_assessment_module import SelfAssess
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
|
||||
from . import test_system
|
||||
from . import get_test_system
|
||||
|
||||
import test_util_open_ended
|
||||
|
||||
@@ -51,7 +51,7 @@ class SelfAssessmentTest(unittest.TestCase):
|
||||
'skip_basic_checks': False,
|
||||
}
|
||||
|
||||
self.module = SelfAssessmentModule(test_system(), self.location,
|
||||
self.module = SelfAssessmentModule(get_test_system(), self.location,
|
||||
self.definition,
|
||||
self.descriptor,
|
||||
static_data)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .import test_system
|
||||
from .import get_test_system
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
|
||||
from xmodule.tests.test_export import DATA_DIR
|
||||
@@ -37,7 +37,7 @@ class DummyModulestore(object):
|
||||
"""
|
||||
A mixin that allows test classes to have convenience functions to get a module given a location
|
||||
"""
|
||||
test_system = test_system()
|
||||
get_test_system = get_test_system()
|
||||
|
||||
def setup_modulestore(self, name):
|
||||
self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
|
||||
|
||||
76
common/lib/xmodule/xmodule/tests/test_video_module.py
Normal file
76
common/lib/xmodule/xmodule/tests/test_video_module.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from xmodule.video_module import VideoDescriptor
|
||||
from .test_import import DummySystem
|
||||
|
||||
|
||||
class VideoDescriptorImportTestCase(unittest.TestCase):
|
||||
"""
|
||||
Make sure that VideoDescriptor can import an old XML-based video correctly.
|
||||
"""
|
||||
|
||||
def test_from_xml(self):
|
||||
module_system = DummySystem(load_error_modules=True)
|
||||
xml_data = '''
|
||||
<video display_name="Test Video"
|
||||
youtube="1.0:p2Q6BrNhdh8,0.75:izygArpw-Qo,1.25:1EeWXzPdhSA,1.5:rABDYkeK0x8"
|
||||
show_captions="false"
|
||||
from="00:00:01"
|
||||
to="00:01:00">
|
||||
<source src="http://www.example.com/source.mp4"/>
|
||||
<track src="http://www.example.com/track"/>
|
||||
</video>
|
||||
'''
|
||||
output = VideoDescriptor.from_xml(xml_data, module_system)
|
||||
self.assertEquals(output.youtube_id_0_75, 'izygArpw-Qo')
|
||||
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
|
||||
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
|
||||
self.assertEquals(output.youtube_id_1_5, 'rABDYkeK0x8')
|
||||
self.assertEquals(output.show_captions, False)
|
||||
self.assertEquals(output.start_time, 1.0)
|
||||
self.assertEquals(output.end_time, 60)
|
||||
self.assertEquals(output.track, 'http://www.example.com/track')
|
||||
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
|
||||
|
||||
def test_from_xml_missing_attributes(self):
|
||||
"""
|
||||
Ensure that attributes have the right values if they aren't
|
||||
explicitly set in XML.
|
||||
"""
|
||||
module_system = DummySystem(load_error_modules=True)
|
||||
xml_data = '''
|
||||
<video display_name="Test Video"
|
||||
youtube="1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA"
|
||||
show_captions="true">
|
||||
<source src="http://www.example.com/source.mp4"/>
|
||||
<track src="http://www.example.com/track"/>
|
||||
</video>
|
||||
'''
|
||||
output = VideoDescriptor.from_xml(xml_data, module_system)
|
||||
self.assertEquals(output.youtube_id_0_75, '')
|
||||
self.assertEquals(output.youtube_id_1_0, 'p2Q6BrNhdh8')
|
||||
self.assertEquals(output.youtube_id_1_25, '1EeWXzPdhSA')
|
||||
self.assertEquals(output.youtube_id_1_5, '')
|
||||
self.assertEquals(output.show_captions, True)
|
||||
self.assertEquals(output.start_time, 0.0)
|
||||
self.assertEquals(output.end_time, 0.0)
|
||||
self.assertEquals(output.track, 'http://www.example.com/track')
|
||||
self.assertEquals(output.source, 'http://www.example.com/source.mp4')
|
||||
|
||||
def test_from_xml_no_attributes(self):
|
||||
"""
|
||||
Make sure settings are correct if none are explicitly set in XML.
|
||||
"""
|
||||
module_system = DummySystem(load_error_modules=True)
|
||||
xml_data = '<video></video>'
|
||||
output = VideoDescriptor.from_xml(xml_data, module_system)
|
||||
self.assertEquals(output.youtube_id_0_75, '')
|
||||
self.assertEquals(output.youtube_id_1_0, 'OEoXaMPEzfM')
|
||||
self.assertEquals(output.youtube_id_1_25, '')
|
||||
self.assertEquals(output.youtube_id_1_5, '')
|
||||
self.assertEquals(output.show_captions, True)
|
||||
self.assertEquals(output.start_time, 0.0)
|
||||
self.assertEquals(output.end_time, 0.0)
|
||||
self.assertEquals(output.track, '')
|
||||
self.assertEquals(output.source, '')
|
||||
@@ -18,9 +18,9 @@ import unittest
|
||||
from mock import Mock
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.video_module import VideoDescriptor, VideoModule
|
||||
from xmodule.video_module import VideoDescriptor, VideoModule, _parse_time, _parse_youtube
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.tests.test_logic import LogicTest
|
||||
|
||||
|
||||
@@ -49,9 +49,9 @@ class VideoFactory(object):
|
||||
"SampleProblem1"])
|
||||
model_data = {'data': VideoFactory.sample_problem_xml_youtube, 'location': location}
|
||||
|
||||
descriptor = Mock(weight="1")
|
||||
descriptor = Mock(weight="1", url_name="SampleProblem1")
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = lambda template, context: context
|
||||
module = VideoModule(system, descriptor, model_data)
|
||||
|
||||
@@ -67,69 +67,57 @@ class VideoModuleLogicTest(LogicTest):
|
||||
'data': '<video />'
|
||||
}
|
||||
|
||||
def test_get_timeframe_no_parameters(self):
|
||||
"""Make sure that timeframe() works correctly w/o parameters"""
|
||||
xmltree = etree.fromstring('<video>test</video>')
|
||||
output = self.xmodule.get_timeframe(xmltree)
|
||||
self.assertEqual(output, ('', ''))
|
||||
def test_parse_time(self):
|
||||
"""Ensure that times are parsed correctly into seconds."""
|
||||
output = _parse_time('00:04:07')
|
||||
self.assertEqual(output, 247)
|
||||
|
||||
def test_get_timeframe_with_one_parameter(self):
|
||||
"""Make sure that timeframe() works correctly with one parameter"""
|
||||
xmltree = etree.fromstring(
|
||||
'<video from="00:04:07">test</video>'
|
||||
)
|
||||
output = self.xmodule.get_timeframe(xmltree)
|
||||
self.assertEqual(output, (247, ''))
|
||||
def test_parse_time_none(self):
|
||||
"""Check parsing of None."""
|
||||
output = _parse_time(None)
|
||||
self.assertEqual(output, '')
|
||||
|
||||
def test_get_timeframe_with_two_parameters(self):
|
||||
"""Make sure that timeframe() works correctly with two parameters"""
|
||||
xmltree = etree.fromstring(
|
||||
'''<video
|
||||
from="00:04:07"
|
||||
to="13:04:39"
|
||||
>test</video>'''
|
||||
)
|
||||
output = self.xmodule.get_timeframe(xmltree)
|
||||
self.assertEqual(output, (247, 47079))
|
||||
def test_parse_time_empty(self):
|
||||
"""Check parsing of the empty string."""
|
||||
output = _parse_time('')
|
||||
self.assertEqual(output, '')
|
||||
|
||||
def test_parse_youtube(self):
|
||||
"""Test parsing old-style Youtube ID strings into a dict."""
|
||||
youtube_str = '0.75:jNCf2gIqpeE,1.00:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg'
|
||||
output = _parse_youtube(youtube_str)
|
||||
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
|
||||
'1.00': 'ZwkTiUPN0mg',
|
||||
'1.25': 'rsq9auxASqI',
|
||||
'1.50': 'kMyNdzVHHgg'})
|
||||
|
||||
class VideoModuleUnitTest(unittest.TestCase):
|
||||
"""Unit tests for Video Xmodule."""
|
||||
def test_parse_youtube_one_video(self):
|
||||
"""
|
||||
Ensure that all keys are present and missing speeds map to the
|
||||
empty string.
|
||||
"""
|
||||
youtube_str = '0.75:jNCf2gIqpeE'
|
||||
output = _parse_youtube(youtube_str)
|
||||
self.assertEqual(output, {'0.75': 'jNCf2gIqpeE',
|
||||
'1.00': '',
|
||||
'1.25': '',
|
||||
'1.50': ''})
|
||||
|
||||
def test_video_constructor(self):
|
||||
"""Make sure that all parameters extracted correclty from xml"""
|
||||
module = VideoFactory.create()
|
||||
def test_parse_youtube_key_format(self):
|
||||
"""
|
||||
Make sure that inconsistent speed keys are parsed correctly.
|
||||
"""
|
||||
youtube_str = '1.00:p2Q6BrNhdh8'
|
||||
youtube_str_hack = '1.0:p2Q6BrNhdh8'
|
||||
self.assertEqual(_parse_youtube(youtube_str), _parse_youtube(youtube_str_hack))
|
||||
|
||||
# `get_html` return only context, cause we
|
||||
# overwrite `system.render_template`
|
||||
context = module.get_html()
|
||||
expected_context = {
|
||||
'track': None,
|
||||
'show_captions': 'true',
|
||||
'display_name': 'SampleProblem1',
|
||||
'id': module.location.html_id(),
|
||||
'end': 3610.0,
|
||||
'caption_asset_path': '/static/subs/',
|
||||
'source': '.../mit-3091x/M-3091X-FA12-L21-3_100.mp4',
|
||||
'streams': '0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg',
|
||||
'normal_speed_video_id': 'ZwkTiUPN0mg',
|
||||
'position': 0,
|
||||
'start': 3603.0
|
||||
}
|
||||
self.assertDictEqual(context, expected_context)
|
||||
|
||||
self.assertEqual(
|
||||
module.youtube,
|
||||
'0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg')
|
||||
|
||||
self.assertEqual(
|
||||
module.video_list(),
|
||||
module.youtube)
|
||||
|
||||
self.assertEqual(
|
||||
module.position,
|
||||
0)
|
||||
|
||||
self.assertDictEqual(
|
||||
json.loads(module.get_instance_state()),
|
||||
{'position': 0})
|
||||
def test_parse_youtube_empty(self):
|
||||
"""
|
||||
Some courses have empty youtube attributes, so we should handle
|
||||
that well.
|
||||
"""
|
||||
self.assertEqual(_parse_youtube(''),
|
||||
{'0.75': '',
|
||||
'1.00': '',
|
||||
'1.25': '',
|
||||
'1.50': ''})
|
||||
|
||||
@@ -6,7 +6,7 @@ from xblock.core import Scope, String, Dict, Boolean, Integer, Float, Any, List
|
||||
from xmodule.fields import Date, Timedelta
|
||||
from xmodule.xml_module import XmlDescriptor, serialize_field, deserialize_field
|
||||
import unittest
|
||||
from .import test_system
|
||||
from .import get_test_system
|
||||
from nose.tools import assert_equals
|
||||
from mock import Mock
|
||||
|
||||
@@ -140,7 +140,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
# Start of helper methods
|
||||
def get_xml_editable_fields(self, model_data):
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return XmlDescriptor(runtime=system, model_data=model_data).editable_metadata_fields
|
||||
|
||||
@@ -152,7 +152,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
non_editable_fields.append(TestModuleDescriptor.due)
|
||||
return non_editable_fields
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return TestModuleDescriptor(runtime=system, model_data=model_data)
|
||||
|
||||
|
||||
@@ -6,23 +6,31 @@ import logging
|
||||
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string, resource_listdir
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from django.http import Http404
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xblock.core import Integer, Scope, String
|
||||
|
||||
import datetime
|
||||
import time
|
||||
from xmodule.editing_module import MetadataOnlyEditingDescriptor
|
||||
from xblock.core import Integer, Scope, String, Float, Boolean
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VideoFields(object):
|
||||
"""Fields for `VideoModule` and `VideoDescriptor`."""
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
position = Integer(help="Current position in the video", scope=Scope.user_state, default=0)
|
||||
show_captions = Boolean(help="This controls whether or not captions are shown by default.", display_name="Show Captions", scope=Scope.settings, default=True)
|
||||
youtube_id_1_0 = String(help="This is the Youtube ID reference for the normal speed video.", display_name="Default Speed", scope=Scope.settings, default="OEoXaMPEzfM")
|
||||
youtube_id_0_75 = String(help="The Youtube ID for the .75x speed video.", display_name="Speed: .75x", scope=Scope.settings, default="")
|
||||
youtube_id_1_25 = String(help="The Youtube ID for the 1.25x speed video.", display_name="Speed: 1.25x", scope=Scope.settings, default="")
|
||||
youtube_id_1_5 = String(help="The Youtube ID for the 1.5x speed video.", display_name="Speed: 1.5x", scope=Scope.settings, default="")
|
||||
start_time = Float(help="Time the video starts", display_name="Start Time", scope=Scope.settings, default=0.0)
|
||||
end_time = Float(help="Time the video ends", display_name="End Time", scope=Scope.settings, default=0.0)
|
||||
source = String(help="The external URL to download the video. This appears as a link beneath the video.", display_name="Download Video", scope=Scope.settings, default="")
|
||||
track = String(help="The external URL to download the subtitle track. This appears as a link beneath the video.", display_name="Download Track", scope=Scope.settings, default="")
|
||||
|
||||
|
||||
class VideoModule(VideoFields, XModule):
|
||||
@@ -46,54 +54,6 @@ class VideoModule(VideoFields, XModule):
|
||||
def __init__(self, *args, **kwargs):
|
||||
XModule.__init__(self, *args, **kwargs)
|
||||
|
||||
xmltree = etree.fromstring(self.data)
|
||||
self.youtube = xmltree.get('youtube')
|
||||
self.show_captions = xmltree.get('show_captions', 'true')
|
||||
self.source = self._get_source(xmltree)
|
||||
self.track = self._get_track(xmltree)
|
||||
self.start_time, self.end_time = self.get_timeframe(xmltree)
|
||||
|
||||
def _get_source(self, xmltree):
|
||||
"""Find the first valid source."""
|
||||
return self._get_first_external(xmltree, 'source')
|
||||
|
||||
def _get_track(self, xmltree):
|
||||
"""Find the first valid track."""
|
||||
return self._get_first_external(xmltree, 'track')
|
||||
|
||||
def _get_first_external(self, xmltree, tag):
|
||||
"""
|
||||
Will return the first valid element
|
||||
of the given tag.
|
||||
'valid' means has a non-empty 'src' attribute
|
||||
"""
|
||||
result = None
|
||||
for element in xmltree.findall(tag):
|
||||
src = element.get('src')
|
||||
if src:
|
||||
result = src
|
||||
break
|
||||
return result
|
||||
|
||||
def get_timeframe(self, xmltree):
|
||||
""" Converts 'from' and 'to' parameters in video tag to seconds.
|
||||
If there are no parameters, returns empty string. """
|
||||
|
||||
def parse_time(str_time):
|
||||
"""Converts s in '12:34:45' format to seconds. If s is
|
||||
None, returns empty string"""
|
||||
if str_time is None:
|
||||
return ''
|
||||
else:
|
||||
obj_time = time.strptime(str_time, '%H:%M:%S')
|
||||
return datetime.timedelta(
|
||||
hours=obj_time.tm_hour,
|
||||
minutes=obj_time.tm_min,
|
||||
seconds=obj_time.tm_sec
|
||||
).total_seconds()
|
||||
|
||||
return parse_time(xmltree.get('from')), parse_time(xmltree.get('to'))
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
"""This is not being called right now and we raise 404 error."""
|
||||
log.debug(u"GET {0}".format(get))
|
||||
@@ -104,38 +64,135 @@ class VideoModule(VideoFields, XModule):
|
||||
"""Return information about state (position)."""
|
||||
return json.dumps({'position': self.position})
|
||||
|
||||
def video_list(self):
|
||||
"""Return video list."""
|
||||
return self.youtube
|
||||
|
||||
def get_html(self):
|
||||
# We normally let JS parse this, but in the case that we need a hacked
|
||||
# out <object> player because YouTube has broken their <iframe> API for
|
||||
# the third time in a year, we need to extract it server side.
|
||||
normal_speed_video_id = None # The 1.0 speed video
|
||||
|
||||
# video_list() example:
|
||||
# "0.75:nugHYNiD3fI,1.0:7m8pab1MfYY,1.25:3CxdPGXShq8,1.50:F-D7bOFCnXA"
|
||||
for video_id_str in self.video_list().split(","):
|
||||
if video_id_str.startswith("1.0:"):
|
||||
normal_speed_video_id = video_id_str.split(":")[1]
|
||||
|
||||
return self.system.render_template('video.html', {
|
||||
'streams': self.video_list(),
|
||||
'youtube_id_0_75': self.youtube_id_0_75,
|
||||
'youtube_id_1_0': self.youtube_id_1_0,
|
||||
'youtube_id_1_25': self.youtube_id_1_25,
|
||||
'youtube_id_1_5': self.youtube_id_1_5,
|
||||
'id': self.location.html_id(),
|
||||
'position': self.position,
|
||||
'source': self.source,
|
||||
'track': self.track,
|
||||
'display_name': self.display_name_with_default,
|
||||
'caption_asset_path': "/static/subs/",
|
||||
'show_captions': self.show_captions,
|
||||
'show_captions': 'true' if self.show_captions else 'false',
|
||||
'start': self.start_time,
|
||||
'end': self.end_time,
|
||||
'normal_speed_video_id': normal_speed_video_id
|
||||
'end': self.end_time
|
||||
})
|
||||
|
||||
|
||||
class VideoDescriptor(VideoFields, RawDescriptor):
|
||||
"""Descriptor for `VideoModule`."""
|
||||
class VideoDescriptor(VideoFields,
|
||||
MetadataOnlyEditingDescriptor,
|
||||
RawDescriptor):
|
||||
module_class = VideoModule
|
||||
template_dir_name = "video"
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(MetadataOnlyEditingDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([VideoModule.start_time,
|
||||
VideoModule.end_time])
|
||||
return non_editable_fields
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Creates an instance of this descriptor from the supplied xml_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
system: A DescriptorSystem for interacting with external resources
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
"""
|
||||
video = super(VideoDescriptor, cls).from_xml(xml_data, system, org, course)
|
||||
xml = etree.fromstring(xml_data)
|
||||
|
||||
display_name = xml.get('display_name')
|
||||
if display_name:
|
||||
video.display_name = display_name
|
||||
|
||||
youtube = xml.get('youtube')
|
||||
if youtube:
|
||||
speeds = _parse_youtube(youtube)
|
||||
if speeds['0.75']:
|
||||
video.youtube_id_0_75 = speeds['0.75']
|
||||
if speeds['1.00']:
|
||||
video.youtube_id_1_0 = speeds['1.00']
|
||||
if speeds['1.25']:
|
||||
video.youtube_id_1_25 = speeds['1.25']
|
||||
if speeds['1.50']:
|
||||
video.youtube_id_1_5 = speeds['1.50']
|
||||
|
||||
show_captions = xml.get('show_captions')
|
||||
if show_captions:
|
||||
video.show_captions = json.loads(show_captions)
|
||||
|
||||
source = _get_first_external(xml, 'source')
|
||||
if source:
|
||||
video.source = source
|
||||
|
||||
track = _get_first_external(xml, 'track')
|
||||
if track:
|
||||
video.track = track
|
||||
|
||||
start_time = _parse_time(xml.get('from'))
|
||||
if start_time:
|
||||
video.start_time = start_time
|
||||
|
||||
end_time = _parse_time(xml.get('to'))
|
||||
if end_time:
|
||||
video.end_time = end_time
|
||||
|
||||
return video
|
||||
|
||||
|
||||
def _get_first_external(xmltree, tag):
|
||||
"""
|
||||
Returns the src attribute of the nested `tag` in `xmltree`, if it
|
||||
exists.
|
||||
"""
|
||||
for element in xmltree.findall(tag):
|
||||
src = element.get('src')
|
||||
if src:
|
||||
return src
|
||||
return None
|
||||
|
||||
|
||||
def _parse_youtube(data):
|
||||
"""
|
||||
Parses a string of Youtube IDs such as "1.0:AXdE34_U,1.5:VO3SxfeD"
|
||||
into a dictionary. Necessary for backwards compatibility with
|
||||
XML-based courses.
|
||||
"""
|
||||
ret = {'0.75': '', '1.00': '', '1.25': '', '1.50': ''}
|
||||
if data == '':
|
||||
return ret
|
||||
videos = data.split(',')
|
||||
for video in videos:
|
||||
pieces = video.split(':')
|
||||
# HACK
|
||||
# To elaborate somewhat: in many LMS tests, the keys for
|
||||
# Youtube IDs are inconsistent. Sometimes a particular
|
||||
# speed isn't present, and formatting is also inconsistent
|
||||
# ('1.0' versus '1.00'). So it's necessary to either do
|
||||
# something like this or update all the tests to work
|
||||
# properly.
|
||||
ret['%.2f' % float(pieces[0])] = pieces[1]
|
||||
return ret
|
||||
|
||||
|
||||
def _parse_time(str_time):
|
||||
"""Converts s in '12:34:45' format to seconds. If s is
|
||||
None, returns empty string"""
|
||||
if str_time is None or str_time == '':
|
||||
return ''
|
||||
else:
|
||||
obj_time = time.strptime(str_time, '%H:%M:%S')
|
||||
return datetime.timedelta(
|
||||
hours=obj_time.tm_hour,
|
||||
minutes=obj_time.tm_min,
|
||||
seconds=obj_time.tm_sec
|
||||
).total_seconds()
|
||||
|
||||
@@ -189,3 +189,10 @@
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// UI archetypes - well
|
||||
.ui-well {
|
||||
@include box-shadow(inset 0 1px 2px 1px $shadow);
|
||||
padding: ($baseline*0.75);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,16 @@
|
||||
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds"
|
||||
showanswer="attempted" rerandomize="never">
|
||||
<video
|
||||
youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM"
|
||||
slug="What_s_next" name="What's next" />
|
||||
youtube_id_0_75=""XNh13VZhThQ""
|
||||
youtube_id_1_0=""XbDRmF6J0K0""
|
||||
youtube_id_1_25=""JDty12WEQWk""
|
||||
youtube_id_1_5=""wELKGj-5iyM""
|
||||
slug="What_s_next"
|
||||
name="What's next"/>
|
||||
<html slug="html_95">Minor correction: Six elements (five resistors)…
|
||||
</html>
|
||||
<customtag tag="S1" slug="discuss_96" impl="discuss" />
|
||||
</vertical>
|
||||
|
||||
<randomize url_name="PS1_Q4" display_name="Problem 4: Read a Molecule">
|
||||
<vertical>
|
||||
<html slug="html_900">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<sequential>
|
||||
<video youtube="1.50:8kARlsUt9lM,1.25:4cLA-IME32w,1.0:pFOrD8k9_p4,0.75:CcgAYu0n0bg" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/>
|
||||
<video youtube_id_1_5=""8kARlsUt9lM"" youtube_id_1_25=""4cLA-IME32w"" youtube_id_1_0=""pFOrD8k9_p4"" youtube_id_0_75=""CcgAYu0n0bg"" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/>
|
||||
<customtag tag="S1" slug="discuss_59" impl="discuss"/>
|
||||
<customtag page="29" slug="book_60" impl="book"/>
|
||||
<customtag lecnum="1" slug="slides_61" impl="slides"/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- UTF-8 characters are acceptable… HTML entities are not -->
|
||||
<h1>Inline content…</h1>
|
||||
</html>
|
||||
<video youtube="1.50:vl9xrfxcr38,1.25:qxNX4REGqx4,1.0:BGU1poJDgOY,0.75:8rK9vnpystQ" slug="S1V14_Summary" name="S1V14: Summary"/>
|
||||
<video youtube_id_1_5=""vl9xrfxcr38"" youtube_id_1_25=""qxNX4REGqx4"" youtube_id_1_0=""BGU1poJDgOY"" youtube_id_0_75=""8rK9vnpystQ"" slug="S1V14_Summary" name="S1V14: Summary"/>
|
||||
<customtag tag="S1" slug="discuss_91" impl="discuss"/>
|
||||
<customtag page="70" slug="book_92" impl="book"/>
|
||||
<customtag lecnum="1" slug="slides_93" impl="slides"/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<sequential>
|
||||
<video youtube="0.75:3NIegrCmA5k,1.0:eLAyO33baQ8,1.25:m1zWi_sh4Aw,1.50:EG-fRTJln_E" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/>
|
||||
<video youtube_id_0_75=""3NIegrCmA5k"" youtube_id_1_0=""eLAyO33baQ8"" youtube_id_1_25=""m1zWi_sh4Aw"" youtube_id_1_5=""EG-fRTJln_E"" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/>
|
||||
<customtag tag="S2" slug="discuss_95" impl="discuss"/>
|
||||
<customtag page="54" slug="book_96" impl="book"/>
|
||||
<customtag lecnum="2" slug="slides_97" impl="slides"/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<sequential>
|
||||
<video youtube="0.75:S_1NaY5te8Q,1.0:G_2F9wivspM,1.25:b-r7dISY-Uc,1.50:jjxHom0oXWk" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/>
|
||||
<video youtube_id_0_75=""S_1NaY5te8Q"" youtube_id_1_0=""G_2F9wivspM"" youtube_id_1_25=""b-r7dISY-Uc"" youtube_id_1_5=""jjxHom0oXWk"" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/>
|
||||
<customtag tag="S2" slug="discuss_99" impl="discuss"/>
|
||||
<customtag page="56" slug="book_100" impl="book"/>
|
||||
<customtag lecnum="2" slug="slides_101" impl="slides"/>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<video youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8" format="Video" display_name="Welcome…"/>
|
||||
<video youtube_id_0_75=""izygArpw-Qo"" youtube_id_1_0=""p2Q6BrNhdh8"" youtube_id_1_25=""1EeWXzPdhSA"" youtube_id_1_5=""rABDYkeK0x8"" format="Video" display_name="Welcome…"/>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<course name="A Simple Course" org="edX" course="simple" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall">
|
||||
<chapter name="Overview">
|
||||
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
|
||||
<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="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
|
||||
<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="1.0:TBvX7HzxexQ"/>
|
||||
<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>
|
||||
@@ -18,7 +18,7 @@
|
||||
<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="1.0:TBvX7HzxexQ"/>
|
||||
<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'>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
<html url_name="toylab"/>
|
||||
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
<video url_name="Video_Resources" youtube_id_1_0=""1bK-WdDi6Qw""/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
<video url_name="Welcome" youtube_id_1_0=""p2Q6BrNhdh8""/>
|
||||
</chapter>
|
||||
<chapter url_name="Ch2">
|
||||
<html url_name="test_html">
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
<html url_name="toylab"/>
|
||||
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
<video url_name="Video_Resources" youtube_id_1_0=""1bK-WdDi6Qw""/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
<video url_name="Welcome" youtube_id_1_0=""p2Q6BrNhdh8""/>
|
||||
</chapter>
|
||||
<chapter url_name="Ch2">
|
||||
<html url_name="test_html">
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<chapter>
|
||||
<video url_name="toyvideo" youtube="blahblah"/>
|
||||
<video url_name="toyvideo" youtube_id_1_0=""OEoXaMPEzfM""/>
|
||||
</chapter>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<chapter url_name="Overview">
|
||||
<videosequence url_name="Toy_Videos">
|
||||
<html url_name="secret:toylab"/>
|
||||
<video url_name="Video_Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
<video url_name="Video_Resources" youtube_id_1_0=""1bK-WdDi6Qw""/>
|
||||
</videosequence>
|
||||
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
<video url_name="video_123456789012" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
<video url_name="video_123456789012" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
<video url_name="Welcome" youtube_id_1_0=""p2Q6BrNhdh8""/>
|
||||
<video url_name="video_123456789012" youtube_id_1_0=""p2Q6BrNhdh8""/>
|
||||
<video url_name="video_4f66f493ac8f" youtube_id_1_0=""p2Q6BrNhdh8""/>
|
||||
</chapter>
|
||||
<chapter url_name="secret:magic"/>
|
||||
</course>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<video youtube="1.0:1bK-WdDi6Qw" display_name="Video Resources"/>
|
||||
<video youtube_id_1_0=""1bK-WdDi6Qw"" display_name="Video Resources"/>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<video youtube="1.0:p2Q6BrNhdh9" display_name="Welcome"/>
|
||||
<video youtube_id_1_0="p2Q6BrNhdh9" display_name="Welcome"/>
|
||||
|
||||
@@ -63,6 +63,25 @@ To get a full list of available rake tasks, use:
|
||||
|
||||
rake -T
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Reference Error: XModule is not defined (javascript)
|
||||
This means that the javascript defining an xmodule hasn't loaded correctly. There are a number
|
||||
of different things that could be causing this:
|
||||
|
||||
1. See `Error: watch EMFILE`
|
||||
|
||||
#### Error: watch EMFILE (coffee)
|
||||
When running a development server, we also start a watcher process alongside to recompile coffeescript
|
||||
and sass as changes are made. On Mac OSX systems, the coffee watcher process takes more file handles
|
||||
than are allowed by default. This will result in `EMFILE` errors when coffeescript is running, and
|
||||
will prevent javascript from compiling, leading to the error 'XModule is not defined'
|
||||
|
||||
To work around this issue, we use `Process::setrlimit` to set the number of allowed open files.
|
||||
Coffee watches both directories and files, so you will need to set this fairly high (anecdotally,
|
||||
8000 seems to do the trick on OSX 10.7.5, 10.8.3, and 10.8.4)
|
||||
|
||||
|
||||
## Running Tests
|
||||
|
||||
See `testing.md` for instructions on running the test suite.
|
||||
|
||||
@@ -122,11 +122,6 @@ In production, the django `collectstatic` command recompiles everything and puts
|
||||
|
||||
In development, we don't use collectstatic, instead accessing the files in place. The auto-compilation is run via `common/djangoapps/pipeline_mako/templates/static_content.html`. Details: templates include `<%namespace name='static' file='static_content.html'/>`, then something like `<%static:css group='application'/>` to call the functions in `common/djangoapps/pipeline_mako/__init__.py`, which call the `django-pipeline` compilers.
|
||||
|
||||
### Other modules
|
||||
|
||||
- Wiki -- in `lms/djangoapps/simplewiki`. Has some markdown extentions for embedding circuits, videos, etc.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
See `testing.md`.
|
||||
|
||||
@@ -23,8 +23,11 @@ be specified for this tag::
|
||||
|
||||
sources - location id of required modules, separated by ';'
|
||||
[message | ""] - message for case, where one or more are not passed. Here you can use variable {link}, which generate link to required module.
|
||||
|
||||
[submitted] - map to `is_submitted` module method.
|
||||
(pressing RESET button makes this function to return False.)
|
||||
|
||||
[completed] - map to `is_completed` module method
|
||||
[correct] - map to `is_correct` module method
|
||||
[attempted] - map to `is_attempted` module method
|
||||
[poll_answer] - map to `poll_answer` module attribute
|
||||
[voted] - map to `voted` module attribute
|
||||
@@ -53,7 +56,7 @@ Examples of conditional depends on poll
|
||||
</conditional>
|
||||
|
||||
Examples of conditional depends on poll (use <show> tag)
|
||||
-------------------------------------------
|
||||
--------------------------------------------------------
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
|
||||
@@ -420,6 +420,6 @@ Draggables can be reused
|
||||
.. literalinclude:: drag-n-drop-demo2.xml
|
||||
|
||||
Examples of targets on draggables
|
||||
------------------------
|
||||
---------------------------------
|
||||
|
||||
.. literalinclude:: drag-n-drop-demo3.xml
|
||||
|
||||
@@ -362,7 +362,7 @@ that has to be updated on a parameter's change, then one can define
|
||||
a special function to handle this. The "output" of such a function must be
|
||||
set to "none", and the JavaScript code inside this function must update the
|
||||
MathJax element by itself. Before exiting, MathJax typeset function should
|
||||
be called so that the new text will be re-rendered by MathJax. For example,
|
||||
be called so that the new text will be re-rendered by MathJax. For example::
|
||||
|
||||
<render>
|
||||
...
|
||||
|
||||
@@ -19,11 +19,11 @@ This is a partial list of features, to be revised as we go along:
|
||||
|
||||
An example of a problem::
|
||||
|
||||
<symbolicresponse expect="a_b^c + b_x__d" size="30">
|
||||
<textline math="1"
|
||||
<symbolicresponse expect="a_b^c + b_x__d" size="30">
|
||||
<textline math="1"
|
||||
preprocessorClassName="SymbolicMathjaxPreprocessor"
|
||||
preprocessorSrc="/static/js/capa/symbolic_mathjax_preprocessor.js"/>
|
||||
</symbolicresponse>
|
||||
</symbolicresponse>
|
||||
|
||||
It's a bit of a pain to enter that.
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ Specific Problem Types
|
||||
course_data_formats/conditional_module/conditional_module.rst
|
||||
course_data_formats/word_cloud/word_cloud.rst
|
||||
course_data_formats/custom_response.rst
|
||||
course_data_formats/symbolic_response.rst
|
||||
|
||||
|
||||
Internal Data Formats
|
||||
|
||||
7
docs/source/calc.rst
Normal file
7
docs/source/calc.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
*******************************************
|
||||
Calc
|
||||
*******************************************
|
||||
|
||||
.. automodule:: calc
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -8,14 +8,6 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
chem.rst
|
||||
|
||||
Calc
|
||||
====
|
||||
|
||||
.. automodule:: capa.calc
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Capa_problem
|
||||
============
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
*******************************************
|
||||
Chem module
|
||||
Chemistry modules
|
||||
*******************************************
|
||||
|
||||
.. module:: chem
|
||||
@@ -7,7 +7,7 @@ Chem module
|
||||
Miller
|
||||
======
|
||||
|
||||
.. automodule:: capa.chem.miller
|
||||
.. automodule:: chem.miller
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -47,14 +47,14 @@ Documentation from **crystallography.js**::
|
||||
Chemcalc
|
||||
========
|
||||
|
||||
.. automodule:: capa.chem.chemcalc
|
||||
.. automodule:: chem.chemcalc
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Chemtools
|
||||
=========
|
||||
|
||||
.. automodule:: capa.chem.chemtools
|
||||
.. automodule:: chem.chemtools
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -62,7 +62,7 @@ Chemtools
|
||||
Tests
|
||||
=====
|
||||
|
||||
.. automodule:: capa.chem.tests
|
||||
.. automodule:: chem.tests
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
@@ -4,86 +4,3 @@ CMS module
|
||||
|
||||
.. module:: cms
|
||||
|
||||
Auth
|
||||
====
|
||||
|
||||
.. automodule:: auth
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Authz
|
||||
-----
|
||||
|
||||
.. automodule:: auth.authz
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Content store
|
||||
=============
|
||||
|
||||
.. .. automodule:: contentstore
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Utils
|
||||
.. -----
|
||||
|
||||
.. .. automodule:: contentstore.untils
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Views
|
||||
.. -----
|
||||
|
||||
.. .. automodule:: contentstore.views
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Management
|
||||
.. ----------
|
||||
|
||||
.. .. automodule:: contentstore.management
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
.. Tests
|
||||
.. -----
|
||||
|
||||
.. .. automodule:: contentstore.tests
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
|
||||
Github sync
|
||||
===========
|
||||
|
||||
.. automodule:: github_sync
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
.. automodule:: github_sync.exceptions
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Views
|
||||
-----
|
||||
|
||||
.. automodule:: github_sync.views
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Management
|
||||
----------
|
||||
|
||||
.. automodule:: github_sync.management
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
.. .. automodule:: github_sync.tests
|
||||
.. :members:
|
||||
.. :show-inheritance:
|
||||
@@ -6,4 +6,9 @@ Contents:
|
||||
:maxdepth: 2
|
||||
|
||||
xmodule.rst
|
||||
capa.rst
|
||||
capa.rst
|
||||
chem.rst
|
||||
sandbox-packages.rst
|
||||
symmath.rst
|
||||
calc.rst
|
||||
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# MITx documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Nov 2 15:43:00 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
#pylint: disable=C0103
|
||||
#pylint: disable=W0622
|
||||
#pylint: disable=W0212
|
||||
#pylint: disable=W0613
|
||||
""" EdX documentation build configuration file, created by
|
||||
sphinx-quickstart on Fri Nov 2 15:43:00 2012.
|
||||
|
||||
import sys, os
|
||||
This file is execfile()d with the current directory set to its containing dir.
|
||||
|
||||
Note that not all possible configuration values are present in this
|
||||
autogenerated file.
|
||||
|
||||
All configuration values have a default; values that are commented out
|
||||
serve to show the default."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.insert(0, os.path.abspath('.'))
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
sys.path.insert(0, os.path.abspath('../..')) # mitx folder
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'capa')) # capa module
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'lib', 'xmodule')) # xmodule
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'lms', 'djangoapps')) # lms djangoapps
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'cms', 'djangoapps')) # cms djangoapps
|
||||
sys.path.insert(0, os.path.join(os.path.abspath('../..'), 'common', 'djangoapps')) # common djangoapps
|
||||
|
||||
# django configuration - careful here
|
||||
import os
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.test'
|
||||
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
@@ -36,7 +34,9 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'lms.envs.dev'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage',
|
||||
'sphinx.ext.pngmath', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -51,17 +51,17 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'MITx'
|
||||
copyright = u'2012, MITx team'
|
||||
project = u'EdX Dev Data'
|
||||
copyright = u'2012-13, EdX team'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.0'
|
||||
version = '0.2'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0'
|
||||
release = '0.2'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -75,7 +75,7 @@ release = '1.0'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
exclude_patterns = ['build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
@@ -175,27 +175,27 @@ html_static_path = ['_static']
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'MITxdoc'
|
||||
htmlhelp_basename = 'edXDocs'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'MITx.tex', u'MITx Documentation',
|
||||
u'MITx team', 'manual'),
|
||||
('index', 'edXDocs.tex', u'EdX Dev Data Documentation',
|
||||
u'EdX Team', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -224,8 +224,8 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'mitx', u'MITx Documentation',
|
||||
[u'MITx team'], 1)
|
||||
('index', 'edxdocs', u'EdX Dev Data Documentation',
|
||||
[u'EdX Team'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@@ -238,9 +238,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'MITx', u'MITx Documentation',
|
||||
u'MITx team', 'MITx', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
('index', 'EdXDocs', u'EdX Dev Data Documentation',
|
||||
u'EdX Team', 'EdXDocs', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
@@ -265,8 +265,12 @@ from django.utils.encoding import force_unicode
|
||||
|
||||
|
||||
def process_docstring(app, what, name, obj, options, lines):
|
||||
"""Autodoc django models"""
|
||||
|
||||
# This causes import errors if left outside the function
|
||||
from django.db import models
|
||||
|
||||
# If you want extract docs from django forms:
|
||||
# from django import forms
|
||||
# from django.forms.models import BaseInlineFormSet
|
||||
|
||||
@@ -326,5 +330,6 @@ def process_docstring(app, what, name, obj, options, lines):
|
||||
|
||||
|
||||
def setup(app):
|
||||
# Register the docstring processor with sphinx
|
||||
"""Setup docsting processors"""
|
||||
#Register the docstring processor with sphinx
|
||||
app.connect('autodoc-process-docstring', process_docstring)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.. MITx documentation master file, created by
|
||||
.. EdX Dev documentation master file, created by
|
||||
sphinx-quickstart on Fri Nov 2 15:43:00 2012.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to MITx's documentation!
|
||||
================================
|
||||
Welcome to EdX's Dev documentation!
|
||||
===================================
|
||||
|
||||
Contents:
|
||||
|
||||
|
||||
@@ -314,34 +314,6 @@ Psychoanalyze
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Simple wiki
|
||||
===========
|
||||
|
||||
.. automodule:: simplewiki
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Models
|
||||
------
|
||||
|
||||
.. automodule:: simplewiki.models
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Views
|
||||
-----
|
||||
|
||||
.. automodule:: simplewiki.views
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
.. automodule:: simplewiki.tests
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Static template view
|
||||
====================
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
*******************************************
|
||||
What the pieces are?
|
||||
Overview
|
||||
*******************************************
|
||||
|
||||
What
|
||||
====
|
||||
|
||||
...
|
||||
This is EdX Dev documentation, mainly extracted from docstrings.
|
||||
Autogenerated by Sphinx from python code.
|
||||
Soon support for JS will be impemented.
|
||||
|
||||
How
|
||||
===
|
||||
|
||||
...
|
||||
|
||||
|
||||
Who
|
||||
===
|
||||
|
||||
|
||||
...
|
||||
11
docs/source/sandbox-packages.rst
Normal file
11
docs/source/sandbox-packages.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
*******************************************
|
||||
Sandbox-packages
|
||||
*******************************************
|
||||
.. module:: sandbox-packages
|
||||
|
||||
Loncapa
|
||||
=======
|
||||
|
||||
.. automodule:: loncapa.loncapa_check
|
||||
:members:
|
||||
:show-inheritance:
|
||||
31
docs/source/symmath.rst
Normal file
31
docs/source/symmath.rst
Normal file
@@ -0,0 +1,31 @@
|
||||
*******************************************
|
||||
Symmath
|
||||
*******************************************
|
||||
|
||||
.. module:: symmath
|
||||
|
||||
|
||||
Formula
|
||||
=======
|
||||
|
||||
.. automodule:: symmath.formula
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Symmath check
|
||||
=============
|
||||
|
||||
.. automodule:: symmath.symmath_check
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Symmath tests
|
||||
=============
|
||||
|
||||
.. automodule:: symmath.test_formula
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. automodule:: symmath.test_symmath_check
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -144,13 +144,6 @@ Templates
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Time parse
|
||||
==========
|
||||
|
||||
.. automodule:: xmodule.timeparse
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Vertical
|
||||
========
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from certificates.models import certificate_status_for_student
|
||||
from certificates.models import CertificateStatuses as status
|
||||
from certificates.models import CertificateWhitelist
|
||||
|
||||
from mitxmako.middleware import MakoMiddleware
|
||||
from courseware import grades, courses
|
||||
from django.test.client import RequestFactory
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
@@ -51,6 +52,14 @@ class XQueueCertInterface(object):
|
||||
"""
|
||||
|
||||
def __init__(self, request=None):
|
||||
# MakoMiddleware Note:
|
||||
# Line below has the side-effect of writing to a module level lookup
|
||||
# table that will allow problems to render themselves. If this is not
|
||||
# present, problems that a student hasn't seen will error when loading,
|
||||
# causing the grading system to under-count the possible score and
|
||||
# inflate their grade. This dependency is bad and was probably recently
|
||||
# introduced. This is the bandage until we can trace the root cause.
|
||||
m = MakoMiddleware()
|
||||
|
||||
# Get basic auth (username/password) for
|
||||
# xqueue connection if it's in the settings
|
||||
@@ -161,6 +170,10 @@ class XQueueCertInterface(object):
|
||||
cert, created = GeneratedCertificate.objects.get_or_create(
|
||||
user=student, course_id=course_id)
|
||||
|
||||
# Needed
|
||||
self.request.user = student
|
||||
self.request.session = {}
|
||||
|
||||
grade = grades.grade(student, self.request, course)
|
||||
is_whitelisted = self.whitelist.filter(
|
||||
user=student, course_id=course_id, whitelist=True).exists()
|
||||
@@ -211,5 +224,5 @@ class XQueueCertInterface(object):
|
||||
(error, msg) = self.xqueue_interface.send_to_queue(
|
||||
header=xheader, body=json.dumps(contents))
|
||||
if error:
|
||||
logger.critical('Unable to add a request to the queue')
|
||||
logger.critical('Unable to add a request to the queue: {} {}'.format(error, msg))
|
||||
raise Exception('Unable to send queue message')
|
||||
|
||||
@@ -83,15 +83,18 @@ def click_on_section(step, section):
|
||||
world.css_click(section_css)
|
||||
|
||||
subid = "ui-accordion-accordion-panel-" + str(int(section) - 1)
|
||||
subsection_css = 'ul[id="%s"]> li > a' % subid
|
||||
subsection_css = 'ul.ui-accordion-content-active[id=\'%s\'] > li > a' % subid
|
||||
prev_url = world.browser.url
|
||||
changed_section = lambda: prev_url != world.browser.url
|
||||
|
||||
#for some reason needed to do it in two steps
|
||||
world.css_find(subsection_css).click()
|
||||
world.css_click(subsection_css, success_condition=changed_section)
|
||||
|
||||
|
||||
@step(u'I click on subsection "([^"]*)"$')
|
||||
def click_on_subsection(step, subsection):
|
||||
subsection_css = 'ul[id="ui-accordion-accordion-panel-0"]> li > a'
|
||||
world.css_find(subsection_css)[int(subsection) - 1].click()
|
||||
world.css_click(subsection_css, index=(int(subsection) - 1))
|
||||
|
||||
|
||||
@step(u'I click on sequence "([^"]*)"$')
|
||||
|
||||
@@ -135,12 +135,10 @@ def action_button_present(_step, buttonname, doesnt_appear):
|
||||
|
||||
@step(u'the button with the label "([^"]*)" does( not)? appear')
|
||||
def button_with_label_present(step, buttonname, doesnt_appear):
|
||||
button_css = 'button span.show-label'
|
||||
elem = world.css_find(button_css).first
|
||||
if doesnt_appear:
|
||||
assert_not_equal(elem.text, buttonname)
|
||||
world.browser.is_text_not_present(buttonname, wait_time=5)
|
||||
else:
|
||||
assert_equal(elem.text, buttonname)
|
||||
world.browser.is_text_present(buttonname, wait_time=5)
|
||||
|
||||
|
||||
@step(u'My "([^"]*)" answer is marked "([^"]*)"')
|
||||
|
||||
@@ -142,34 +142,34 @@ def answer_problem(problem_type, correctness):
|
||||
|
||||
elif problem_type == "multiple choice":
|
||||
if correctness == 'correct':
|
||||
inputfield('multiple choice', choice='choice_2').check()
|
||||
world.css_check(inputfield('multiple choice', choice='choice_2'))
|
||||
else:
|
||||
inputfield('multiple choice', choice='choice_1').check()
|
||||
world.css_check(inputfield('multiple choice', choice='choice_1'))
|
||||
|
||||
elif problem_type == "checkbox":
|
||||
if correctness == 'correct':
|
||||
inputfield('checkbox', choice='choice_0').check()
|
||||
inputfield('checkbox', choice='choice_2').check()
|
||||
world.css_check(inputfield('checkbox', choice='choice_0'))
|
||||
world.css_check(inputfield('checkbox', choice='choice_2'))
|
||||
else:
|
||||
inputfield('checkbox', choice='choice_3').check()
|
||||
world.css_check(inputfield('checkbox', choice='choice_3'))
|
||||
|
||||
elif problem_type == 'radio':
|
||||
if correctness == 'correct':
|
||||
inputfield('radio', choice='choice_2').check()
|
||||
world.css_check(inputfield('radio', choice='choice_2'))
|
||||
else:
|
||||
inputfield('radio', choice='choice_1').check()
|
||||
world.css_check(inputfield('radio', choice='choice_1'))
|
||||
|
||||
elif problem_type == 'string':
|
||||
textvalue = 'correct string' if correctness == 'correct' else 'incorrect'
|
||||
inputfield('string').fill(textvalue)
|
||||
world.css_fill(inputfield('string'), textvalue)
|
||||
|
||||
elif problem_type == 'numerical':
|
||||
textvalue = "pi + 1" if correctness == 'correct' else str(random.randint(-2, 2))
|
||||
inputfield('numerical').fill(textvalue)
|
||||
world.css_fill(inputfield('numerical'), textvalue)
|
||||
|
||||
elif problem_type == 'formula':
|
||||
textvalue = "x^2+2*x+y" if correctness == 'correct' else 'x^2'
|
||||
inputfield('formula').fill(textvalue)
|
||||
world.css_fill(inputfield('formula'), textvalue)
|
||||
|
||||
elif problem_type == 'script':
|
||||
# Correct answer is any two integers that sum to 10
|
||||
@@ -181,8 +181,8 @@ def answer_problem(problem_type, correctness):
|
||||
if correctness == 'incorrect':
|
||||
second_addend += random.randint(1, 10)
|
||||
|
||||
inputfield('script', input_num=1).fill(str(first_addend))
|
||||
inputfield('script', input_num=2).fill(str(second_addend))
|
||||
world.css_fill(inputfield('script', input_num=1), str(first_addend))
|
||||
world.css_fill(inputfield('script', input_num=2), str(second_addend))
|
||||
|
||||
elif problem_type == 'code':
|
||||
# The fake xqueue server is configured to respond
|
||||
@@ -281,11 +281,11 @@ def add_problem_to_course(course, problem_type, extraMeta=None):
|
||||
|
||||
|
||||
def inputfield(problem_type, choice=None, input_num=1):
|
||||
""" Return the <input> element for *problem_type*.
|
||||
""" Return the css selector for `problem_type`.
|
||||
For example, if problem_type is 'string', return
|
||||
the text field for the string problem in the test course.
|
||||
|
||||
*choice* is the name of the checkbox input in a group
|
||||
`choice` is the name of the checkbox input in a group
|
||||
of checkboxes. """
|
||||
|
||||
sel = ("input#input_i4x-edx-model_course-problem-%s_2_%s" %
|
||||
@@ -299,7 +299,7 @@ def inputfield(problem_type, choice=None, input_num=1):
|
||||
assert world.is_css_present(sel)
|
||||
|
||||
# Retrieve the input element
|
||||
return world.browser.find_by_css(sel)
|
||||
return sel
|
||||
|
||||
|
||||
def assert_checked(problem_type, choices):
|
||||
@@ -312,7 +312,7 @@ def assert_checked(problem_type, choices):
|
||||
|
||||
all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3']
|
||||
for this_choice in all_choices:
|
||||
element = inputfield(problem_type, choice=this_choice)
|
||||
element = world.css_find(inputfield(problem_type, choice=this_choice))
|
||||
|
||||
if this_choice in choices:
|
||||
assert element.checked
|
||||
@@ -321,5 +321,5 @@ def assert_checked(problem_type, choices):
|
||||
|
||||
|
||||
def assert_textfield(problem_type, expected_text, input_num=1):
|
||||
element = inputfield(problem_type, input_num=input_num)
|
||||
element = world.css_find(inputfield(problem_type, input_num=input_num))
|
||||
assert element.value == expected_text
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
Feature: Video component
|
||||
As a student, I want to view course videos in LMS.
|
||||
|
||||
Scenario: Autoplay is enabled in LMS
|
||||
Given the course has a Video component
|
||||
Then when I view the video it has autoplay enabled
|
||||
Scenario: Autoplay is enabled in LMS for a Video component
|
||||
Given the course has a Video component
|
||||
Then when I view the video it has autoplay enabled
|
||||
|
||||
Scenario: Autoplay is enabled in the LMS for a VideoAlpha component
|
||||
Given the course has a VideoAlpha component
|
||||
Then when I view the video it has autoplay enabled
|
||||
|
||||
@@ -27,8 +27,30 @@ def view_video(_step):
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
@step('the course has a VideoAlpha component')
|
||||
def view_videoalpha(step):
|
||||
coursename = TEST_COURSE_NAME.replace(' ', '_')
|
||||
i_am_registered_for_the_course(step, coursename)
|
||||
|
||||
# Make sure we have a videoalpha
|
||||
add_videoalpha_to_course(coursename)
|
||||
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
|
||||
section_name = chapter_name
|
||||
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
|
||||
(chapter_name, section_name))
|
||||
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
def add_video_to_course(course):
|
||||
template_name = 'i4x://edx/templates/video/default'
|
||||
world.ItemFactory.create(parent_location=section_location(course),
|
||||
template=template_name,
|
||||
display_name='Video')
|
||||
|
||||
|
||||
def add_videoalpha_to_course(course):
|
||||
template_name = 'i4x://edx/templates/videoalpha/Video_Alpha'
|
||||
world.ItemFactory.create(parent_location=section_location(course),
|
||||
template=template_name,
|
||||
display_name='Video Alpha')
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Feature: Video Alpha component
|
||||
As a student, I want to view course videos in LMS.
|
||||
|
||||
Scenario: Autoplay is enabled in LMS
|
||||
Given the course has a Video component
|
||||
Then when I view the video it has autoplay enabled
|
||||
@@ -1,36 +0,0 @@
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=W0613
|
||||
#pylint: disable=W0621
|
||||
|
||||
from lettuce import world, step
|
||||
from lettuce.django import django_url
|
||||
from common import TEST_COURSE_NAME, TEST_SECTION_NAME, i_am_registered_for_the_course, section_location
|
||||
|
||||
############### ACTIONS ####################
|
||||
|
||||
|
||||
@step('when I view the video it has autoplay enabled')
|
||||
def does_autoplay(step):
|
||||
assert(world.css_find('.videoalpha')[0]['data-autoplay'] == 'True')
|
||||
|
||||
|
||||
@step('the course has a Video component')
|
||||
def view_videoalpha(step):
|
||||
coursename = TEST_COURSE_NAME.replace(' ', '_')
|
||||
i_am_registered_for_the_course(step, coursename)
|
||||
|
||||
# Make sure we have a videoalpha
|
||||
add_videoalpha_to_course(coursename)
|
||||
chapter_name = TEST_SECTION_NAME.replace(" ", "_")
|
||||
section_name = chapter_name
|
||||
url = django_url('/courses/edx/Test_Course/Test_Course/courseware/%s/%s' %
|
||||
(chapter_name, section_name))
|
||||
|
||||
world.browser.visit(url)
|
||||
|
||||
|
||||
def add_videoalpha_to_course(course):
|
||||
template_name = 'i4x://edx/templates/videoalpha/default'
|
||||
world.ItemFactory.create(parent_location=section_location(course),
|
||||
template=template_name,
|
||||
display_name='Video Alpha 1')
|
||||
@@ -163,7 +163,7 @@ class ModelDataCache(object):
|
||||
return self._chunked_query(
|
||||
XModuleStudentPrefsField,
|
||||
'module_type__in',
|
||||
set(descriptor.location.category for descriptor in self.descriptors),
|
||||
set(descriptor.module_class.__name__ for descriptor in self.descriptors),
|
||||
student=self.user.pk,
|
||||
field_name__in=set(field.name for field in fields),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.test.client import Client
|
||||
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -77,7 +77,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
data=self.DATA
|
||||
)
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = lambda template, context: context
|
||||
model_data = {'location': self.item_descriptor.location}
|
||||
model_data.update(self.MODEL_DATA)
|
||||
|
||||
@@ -75,7 +75,7 @@ class StudentPrefsFactory(DjangoModelFactory):
|
||||
field_name = 'existing_field'
|
||||
value = json.dumps('old_value')
|
||||
student = SubFactory(UserFactory)
|
||||
module_type = 'problem'
|
||||
module_type = 'MockProblemModule'
|
||||
|
||||
|
||||
class StudentInfoFactory(DjangoModelFactory):
|
||||
|
||||
@@ -29,6 +29,7 @@ def mock_descriptor(fields=[], lms_fields=[]):
|
||||
descriptor.location = location('def_id')
|
||||
descriptor.module_class.fields = fields
|
||||
descriptor.module_class.lms.fields = lms_fields
|
||||
descriptor.module_class.__name__ = 'MockProblemModule'
|
||||
return descriptor
|
||||
|
||||
location = partial(Location, 'i4x', 'edX', 'test_course', 'problem')
|
||||
@@ -37,7 +38,7 @@ course_id = 'edX/test_course/test'
|
||||
content_key = partial(LmsKeyValueStore.Key, Scope.content, None, location('def_id'))
|
||||
settings_key = partial(LmsKeyValueStore.Key, Scope.settings, None, location('def_id'))
|
||||
user_state_key = partial(LmsKeyValueStore.Key, Scope.user_state, 'user', location('def_id'))
|
||||
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'problem')
|
||||
prefs_key = partial(LmsKeyValueStore.Key, Scope.preferences, 'user', 'MockProblemModule')
|
||||
user_info_key = partial(LmsKeyValueStore.Key, Scope.user_info, 'user', None)
|
||||
|
||||
|
||||
@@ -190,6 +191,10 @@ class StorageTestBase(object):
|
||||
self.mdc = ModelDataCache([mock_descriptor([mock_field(self.scope, 'existing_field')])], course_id, self.user)
|
||||
self.kvs = LmsKeyValueStore(self.desc_md, self.mdc)
|
||||
|
||||
def test_set_and_get_existing_field(self):
|
||||
self.kvs.set(self.key_factory('existing_field'), 'test_value')
|
||||
self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field')))
|
||||
|
||||
def test_get_existing_field(self):
|
||||
"Test that getting an existing field in an existing Storage Field works"
|
||||
self.assertEquals('old_value', self.kvs.get(self.key_factory('existing_field')))
|
||||
|
||||
@@ -22,7 +22,7 @@ from django.conf import settings
|
||||
|
||||
from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.tests import test_system
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.tests.test_logic import LogicTest
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class VideoAlphaFactory(object):
|
||||
|
||||
descriptor = Mock(weight="1")
|
||||
|
||||
system = test_system()
|
||||
system = get_test_system()
|
||||
system.render_template = lambda template, context: context
|
||||
VideoAlphaModule.location = location
|
||||
module = VideoAlphaModule(system, descriptor, model_data)
|
||||
|
||||
@@ -95,13 +95,19 @@ class FolditTestCase(TestCase):
|
||||
response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000])
|
||||
|
||||
self.assertEqual(response.content, json.dumps(
|
||||
[{"OperationID": "SetPlayerPuzzleScores",
|
||||
"Value": [{
|
||||
"PuzzleID": 1,
|
||||
"Status": "Success"},
|
||||
|
||||
{"PuzzleID": 2,
|
||||
"Status": "Success"}]}]))
|
||||
[{
|
||||
"OperationID": "SetPlayerPuzzleScores",
|
||||
"Value": [
|
||||
{
|
||||
"PuzzleID": 1,
|
||||
"Status": "Success"
|
||||
}, {
|
||||
"PuzzleID": 2,
|
||||
"Status": "Success"
|
||||
}
|
||||
]
|
||||
}]
|
||||
))
|
||||
|
||||
|
||||
def test_SetPlayerPuzzleScores_multiple(self):
|
||||
@@ -126,9 +132,11 @@ class FolditTestCase(TestCase):
|
||||
self.assertEqual(len(top_10), 1)
|
||||
|
||||
# Floats always get in the way, so do almostequal
|
||||
self.assertAlmostEqual(top_10[0]['score'],
|
||||
Score.display_score(better_score),
|
||||
delta=0.5)
|
||||
self.assertAlmostEqual(
|
||||
top_10[0]['score'],
|
||||
Score.display_score(better_score),
|
||||
delta=0.5
|
||||
)
|
||||
|
||||
# reporting a worse score shouldn't
|
||||
worse_score = 0.065
|
||||
@@ -137,9 +145,11 @@ class FolditTestCase(TestCase):
|
||||
top_10 = Score.get_tops_n(10, puzzle_id)
|
||||
self.assertEqual(len(top_10), 1)
|
||||
# should still be the better score
|
||||
self.assertAlmostEqual(top_10[0]['score'],
|
||||
Score.display_score(better_score),
|
||||
delta=0.5)
|
||||
self.assertAlmostEqual(
|
||||
top_10[0]['score'],
|
||||
Score.display_score(better_score),
|
||||
delta=0.5
|
||||
)
|
||||
|
||||
def test_SetPlayerPuzzleScores_manyplayers(self):
|
||||
"""
|
||||
@@ -150,28 +160,34 @@ class FolditTestCase(TestCase):
|
||||
puzzle_id = ['1']
|
||||
player1_score = 0.08
|
||||
player2_score = 0.02
|
||||
response1 = self.make_puzzle_score_request(puzzle_id, player1_score,
|
||||
self.user)
|
||||
response1 = self.make_puzzle_score_request(
|
||||
puzzle_id, player1_score, self.user
|
||||
)
|
||||
|
||||
# There should now be a score in the db.
|
||||
top_10 = Score.get_tops_n(10, puzzle_id)
|
||||
self.assertEqual(len(top_10), 1)
|
||||
self.assertEqual(top_10[0]['score'], Score.display_score(player1_score))
|
||||
|
||||
response2 = self.make_puzzle_score_request(puzzle_id, player2_score,
|
||||
self.user2)
|
||||
response2 = self.make_puzzle_score_request(
|
||||
puzzle_id, player2_score, self.user2
|
||||
)
|
||||
|
||||
# There should now be two scores in the db
|
||||
top_10 = Score.get_tops_n(10, puzzle_id)
|
||||
self.assertEqual(len(top_10), 2)
|
||||
|
||||
# Top score should be player2_score. Second should be player1_score
|
||||
self.assertAlmostEqual(top_10[0]['score'],
|
||||
Score.display_score(player2_score),
|
||||
delta=0.5)
|
||||
self.assertAlmostEqual(top_10[1]['score'],
|
||||
Score.display_score(player1_score),
|
||||
delta=0.5)
|
||||
self.assertAlmostEqual(
|
||||
top_10[0]['score'],
|
||||
Score.display_score(player2_score),
|
||||
delta=0.5
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
top_10[1]['score'],
|
||||
Score.display_score(player1_score),
|
||||
delta=0.5
|
||||
)
|
||||
|
||||
# Top score user should be self.user2.username
|
||||
self.assertEqual(top_10[0]['username'], self.user2.username)
|
||||
|
||||
@@ -36,9 +36,13 @@ def foldit_ops(request):
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"})
|
||||
log.warning("Verification of SetPlayerPuzzleScores failed:" +
|
||||
"user %s, scores json %r, verify %r",
|
||||
request.user, puzzle_scores_json, pz_verify_json)
|
||||
log.warning(
|
||||
"Verification of SetPlayerPuzzleScores failed:"
|
||||
"user %s, scores json %r, verify %r",
|
||||
request.user,
|
||||
puzzle_scores_json,
|
||||
pz_verify_json
|
||||
)
|
||||
else:
|
||||
# This is needed because we are not getting valid json - the
|
||||
# value of ScoreType is an unquoted string. Right now regexes are
|
||||
@@ -65,9 +69,13 @@ def foldit_ops(request):
|
||||
"Success": "false",
|
||||
"ErrorString": "Verification failed",
|
||||
"ErrorCode": "VerifyFailed"})
|
||||
log.warning("Verification of SetPuzzlesComplete failed:" +
|
||||
" user %s, puzzles json %r, verify %r",
|
||||
request.user, puzzles_complete_json, pc_verify_json)
|
||||
log.warning(
|
||||
"Verification of SetPuzzlesComplete failed:"
|
||||
" user %s, puzzles json %r, verify %r",
|
||||
request.user,
|
||||
puzzles_complete_json,
|
||||
pc_verify_json
|
||||
)
|
||||
else:
|
||||
puzzles_complete = json.loads(puzzles_complete_json)
|
||||
responses.append(save_complete(request.user, puzzles_complete))
|
||||
|
||||
@@ -84,13 +84,15 @@ class InstructorTask(models.Model):
|
||||
raise ValueError(msg)
|
||||
|
||||
# create the task, then save it:
|
||||
instructor_task = cls(course_id=course_id,
|
||||
task_type=task_type,
|
||||
task_id=task_id,
|
||||
task_key=task_key,
|
||||
task_input=json_task_input,
|
||||
task_state=QUEUING,
|
||||
requester=requester)
|
||||
instructor_task = cls(
|
||||
course_id=course_id,
|
||||
task_type=task_type,
|
||||
task_id=task_id,
|
||||
task_key=task_key,
|
||||
task_input=json_task_input,
|
||||
task_state=QUEUING,
|
||||
requester=requester
|
||||
)
|
||||
instructor_task.save_now()
|
||||
|
||||
return instructor_task
|
||||
|
||||
@@ -118,10 +118,12 @@ def manage_modulestores(request, reload_dir=None, commit_id=None):
|
||||
html += '<h2>Courses loaded in the modulestore</h2>'
|
||||
html += '<ol>'
|
||||
for cdir, course in def_ms.courses.items():
|
||||
html += '<li><a href="%s/migrate/reload/%s">%s</a> (%s)</li>' % (settings.MITX_ROOT_URL,
|
||||
escape(cdir),
|
||||
escape(cdir),
|
||||
course.location.url())
|
||||
html += '<li><a href="%s/migrate/reload/%s">%s</a> (%s)</li>' % (
|
||||
settings.MITX_ROOT_URL,
|
||||
escape(cdir),
|
||||
escape(cdir),
|
||||
course.location.url()
|
||||
)
|
||||
html += '</ol>'
|
||||
|
||||
#----------------------------------------
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# keys being the COURSE_NAME (spaces ok), and the value being a dict of
|
||||
# parameter,value pairs. The required parameters are:
|
||||
#
|
||||
# - number : course number (used in the simplewiki pages)
|
||||
# - number : course number (used in the wiki pages)
|
||||
# - title : humanized descriptive course title
|
||||
#
|
||||
# Optional parameters:
|
||||
|
||||
@@ -270,8 +270,10 @@ def get_problem_list(request, course_id):
|
||||
mimetype="application/json")
|
||||
except GradingServiceError:
|
||||
#This is a dev_facing_error
|
||||
log.exception("Error from staff grading service in open ended grading. server url: {0}"
|
||||
.format(staff_grading_service().url))
|
||||
log.exception(
|
||||
"Error from staff grading service in open "
|
||||
"ended grading. server url: {0}".format(staff_grading_service().url)
|
||||
)
|
||||
#This is a staff_facing_error
|
||||
return HttpResponse(json.dumps({'success': False,
|
||||
'error': STAFF_ERROR_MESSAGE}))
|
||||
@@ -285,8 +287,10 @@ def _get_next(course_id, grader_id, location):
|
||||
return staff_grading_service().get_next(course_id, location, grader_id)
|
||||
except GradingServiceError:
|
||||
#This is a dev facing error
|
||||
log.exception("Error from staff grading service in open ended grading. server url: {0}"
|
||||
.format(staff_grading_service().url))
|
||||
log.exception(
|
||||
"Error from staff grading service in open "
|
||||
"ended grading. server url: {0}".format(staff_grading_service().url)
|
||||
)
|
||||
#This is a staff_facing_error
|
||||
return json.dumps({'success': False,
|
||||
'error': STAFF_ERROR_MESSAGE})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user