Commit ec4547b5 by JonahStanley

Merge branch 'master' into jonahstanley/add-courseteam-tests

parents 70d48e2e d06a9a20
......@@ -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>
\ No newline at end of file
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`
......
......@@ -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")
......
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"
#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]
......
"""
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
});
}
\ No newline at end of file
}
......@@ -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;
......
// 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>
......
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:
......
......@@ -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,58 @@ 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.
# If this happens, wait a second, then try again
world.wait(1)
attempt += 1
except:
attempt += 1
return result
@world.absorb
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.
......@@ -83,15 +122,15 @@ def css_click(css_selector, index=0, attempts=5):
@world.absorb
def css_click_at(css, x=10, y=10):
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")
......
"""
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='2_2_1',
correctness='incorrect',
npoints=None,
msg=None,
hint=None,
hintmode=None,
queuestate=None)
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
)
# 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='2_2_1',
correctness='correct',
npoints=None)
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='5_2_1',
correctness='correct',
npoints=0)
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='3_2_1',
correctness='incorrect',
npoints=5
)
self.cmap.set(
answer_id='4_2_1',
correctness='incorrect',
npoints=None
)
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())
# 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()
)
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>
\ No newline at end of file
</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():
......
---
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
......
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)
\ No newline at end of file
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)
......
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])
......
# -*- 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_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_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))
class VideoModuleUnitTest(unittest.TestCase):
"""Unit tests for Video Xmodule."""
def test_video_constructor(self):
"""Make sure that all parameters extracted correclty from xml"""
module = VideoFactory.create()
# `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_time(self):
"""Ensure that times are parsed correctly into seconds."""
output = _parse_time('00:04:07')
self.assertEqual(output, 247)
def test_parse_time_none(self):
"""Check parsing of None."""
output = _parse_time(None)
self.assertEqual(output, '')
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'})
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_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))
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="&quot;XNh13VZhThQ&quot;"
youtube_id_1_0="&quot;XbDRmF6J0K0&quot;"
youtube_id_1_25="&quot;JDty12WEQWk&quot;"
youtube_id_1_5="&quot;wELKGj-5iyM&quot;"
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">
......
<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="&quot;8kARlsUt9lM&quot;" youtube_id_1_25="&quot;4cLA-IME32w&quot;" youtube_id_1_0="&quot;pFOrD8k9_p4&quot;" youtube_id_0_75="&quot;CcgAYu0n0bg&quot;" 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="&quot;vl9xrfxcr38&quot;" youtube_id_1_25="&quot;qxNX4REGqx4&quot;" youtube_id_1_0="&quot;BGU1poJDgOY&quot;" youtube_id_0_75="&quot;8rK9vnpystQ&quot;" 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"/>
......
<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="&quot;3NIegrCmA5k&quot;" youtube_id_1_0="&quot;eLAyO33baQ8&quot;" youtube_id_1_25="&quot;m1zWi_sh4Aw&quot;" youtube_id_1_5="&quot;EG-fRTJln_E&quot;" 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"/>
......
<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="&quot;S_1NaY5te8Q&quot;" youtube_id_1_0="&quot;G_2F9wivspM&quot;" youtube_id_1_25="&quot;b-r7dISY-Uc&quot;" youtube_id_1_5="&quot;jjxHom0oXWk&quot;" 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"/>
......
<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="&quot;izygArpw-Qo&quot;" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;" youtube_id_1_25="&quot;1EeWXzPdhSA&quot;" youtube_id_1_5="&quot;rABDYkeK0x8&quot;" format="Video" display_name="Welcome…"/>
<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="&quot;izygArpw-Qo&quot;" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;" youtube_id_1_25="&quot;1EeWXzPdhSA&quot;" youtube_id_1_5="&quot;rABDYkeK0x8&quot;"/>
<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="&quot;EuzkdzfR0i8&quot;" youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;" youtube_id_1_25="&quot;0v1VzoDVUTM&quot;" youtube_id_1_5="&quot;Bxk_-ZJb240&quot;"/>
</videosequence>
<section name="Lecture 2">
<sequential>
<video youtube="1.0:TBvX7HzxexQ"/>
<video youtube_id_1_0="&quot;TBvX7HzxexQ&quot;"/>
<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="&quot;TBvX7HzxexQ&quot;"/>
<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="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
<video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</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="&quot;1bK-WdDi6Qw&quot;"/>
</videosequence>
<video url_name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
<video url_name="Welcome" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter>
<chapter url_name="Ch2">
<html url_name="test_html">
......
<chapter>
<video url_name="toyvideo" youtube="blahblah"/>
<video url_name="toyvideo" youtube_id_1_0="&quot;OEoXaMPEzfM&quot;"/>
</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="&quot;1bK-WdDi6Qw&quot;"/>
</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="&quot;p2Q6BrNhdh8&quot;"/>
<video url_name="video_123456789012" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
<video url_name="video_4f66f493ac8f" youtube_id_1_0="&quot;p2Q6BrNhdh8&quot;"/>
</chapter>
<chapter url_name="secret:magic"/>
</course>
<video youtube="1.0:1bK-WdDi6Qw" display_name="Video Resources"/>
<video youtube_id_1_0="&quot;1bK-WdDi6Qw&quot;" display_name="Video Resources"/>
<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
......
*******************************************
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
============
......
*******************************************
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:
\ No newline at end of file
......@@ -6,4 +6,9 @@ Contents:
:maxdepth: 2
xmodule.rst
capa.rst
\ No newline at end of file
capa.rst
chem.rst
sandbox-packages.rst
symmath.rst
calc.rst
# -*- 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.
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.
import sys, os
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)
.. 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
====================
......
*******************************************
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
===
...
\ No newline at end of file
*******************************************
Sandbox-packages
*******************************************
.. module:: sandbox-packages
Loncapa
=======
.. automodule:: loncapa.loncapa_check
:members:
:show-inheritance:
\ No newline at end of file
*******************************************
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:
\ No newline at end of file
......@@ -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
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')
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
#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})
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment