Commit ae517fee by Peter Fogg

Merge pull request #455 from edx/peter-fogg/remove-videoalpha-xml

Peter fogg/remove videoalpha xml
parents c4d6102a 9c844405
...@@ -42,6 +42,9 @@ Common: Add a manage.py that knows about edx-platform specific settings and proj ...@@ -42,6 +42,9 @@ Common: Add a manage.py that knows about edx-platform specific settings and proj
Common: Added *experimental* support for jsinput type. Common: Added *experimental* support for jsinput type.
Studio: Remove XML from HTML5 video component editor. All settings are
moved to be edited as metadata.
Common: Added setting to specify Celery Broker vhost Common: Added setting to specify Celery Broker vhost
Common: Utilize new XBlock bulk save API in LMS and CMS. Common: Utilize new XBlock bulk save API in LMS and CMS.
......
...@@ -228,6 +228,26 @@ def i_created_a_video_component(step): ...@@ -228,6 +228,26 @@ def i_created_a_video_component(step):
) )
@step('I have created a Video Alpha component$')
def i_created_video_alpha(step):
step.given('I have enabled the videoalpha advanced module')
world.css_click('a.course-link')
step.given('I have added a new subsection')
step.given('I expand the first section')
world.css_click('a.new-unit-item')
world.css_click('.large-advanced-icon')
world.click_component_from_menu('videoalpha', None, '.xmodule_VideoAlphaModule')
@step('I have enabled the (.*) advanced module$')
def i_enabled_the_advanced_module(step, module):
step.given('I have opened a new course section in Studio')
world.css_click('.nav-course-settings')
world.css_click('.nav-course-settings-advanced')
type_in_codemirror(0, '["%s"]' % module)
press_the_notification_button(step, 'Save')
@step('I have clicked the new unit button') @step('I have clicked the new unit button')
def open_new_unit(step): def open_new_unit(step):
step.given('I have opened a new course section in Studio') step.given('I have opened a new course section in Studio')
...@@ -236,14 +256,14 @@ def open_new_unit(step): ...@@ -236,14 +256,14 @@ def open_new_unit(step):
world.css_click('a.new-unit-item') world.css_click('a.new-unit-item')
@step('when I view the video it (.*) show the captions') @step('when I view the (video.*) it (.*) show the captions')
def shows_captions(step, show_captions): def shows_captions(_step, video_type, show_captions):
# Prevent cookies from overriding course settings # Prevent cookies from overriding course settings
world.browser.cookies.delete('hide_captions') world.browser.cookies.delete('hide_captions')
if show_captions == 'does not': if show_captions == 'does not':
assert world.css_has_class('.video', 'closed') assert world.css_has_class('.%s' % video_type, 'closed')
else: else:
assert world.is_css_not_present('.video.closed') assert world.is_css_not_present('.%s.closed' % video_type)
@step('the save button is disabled$') @step('the save button is disabled$')
......
...@@ -60,6 +60,12 @@ def edit_component_and_select_settings(): ...@@ -60,6 +60,12 @@ def edit_component_and_select_settings():
@world.absorb @world.absorb
def edit_component():
world.wait_for(lambda _driver: world.css_visible('a.edit-button'))
world.css_click('a.edit-button')
@world.absorb
def verify_setting_entry(setting, display_name, value, explicitly_set): def verify_setting_entry(setting, display_name, value, explicitly_set):
assert_equal(display_name, setting.find_by_css('.setting-label')[0].value) assert_equal(display_name, setting.find_by_css('.setting-label')[0].value)
assert_equal(value, setting.find_by_css('.setting-input')[0].value) assert_equal(value, setting.find_by_css('.setting-input')[0].value)
......
...@@ -22,3 +22,19 @@ def set_show_captions(step, setting): ...@@ -22,3 +22,19 @@ def set_show_captions(step, setting):
world.wait_for(lambda _driver: world.css_visible('a.save-button')) world.wait_for(lambda _driver: world.css_visible('a.save-button'))
world.browser.select('Show Captions', setting) world.browser.select('Show Captions', setting)
world.css_click('a.save-button') world.css_click('a.save-button')
@step('I see the correct videoalpha settings and default values$')
def correct_videoalpha_settings(_step):
world.verify_all_setting_entries([['Display Name', 'Video Alpha', False],
['Download Track', '', False],
['Download Video', '', False],
['End Time', '0', False],
['HTML5 Subtitles', '', False],
['Show Captions', 'True', False],
['Start Time', '0', False],
['Video Sources', '', False],
['Youtube ID', 'OEoXaMPEzfM', False],
['Youtube ID for .75x speed', '', False],
['Youtube ID for 1.25x speed', '', False],
['Youtube ID for 1.5x speed', '', False]])
...@@ -22,3 +22,28 @@ Feature: Video Component ...@@ -22,3 +22,28 @@ Feature: Video Component
Given I have created a Video component Given I have created a Video component
And I have toggled captions And I have toggled captions
Then when I view the video it does show the captions Then when I view the video it does show the captions
Scenario: Autoplay is disabled in Studio for Video Alpha
Given I have created a Video Alpha component
Then when I view the videoalpha it does not have autoplay enabled
Scenario: User can view Video Alpha metadata
Given I have created a Video Alpha component
And I edit the component
Then I see the correct videoalpha settings and default values
Scenario: User can modify Video Alpha display name
Given I have created a Video Alpha component
And I edit the component
Then I can modify the display name
And my videoalpha display name change is persisted on save
Scenario: Video Alpha captions are hidden when "show captions" is false
Given I have created a Video Alpha component
And I have set "show captions" to False
Then when I view the videoalpha it does not show the captions
Scenario: Video Alpha captions are shown when "show captions" is true
Given I have created a Video Alpha component
And I have set "show captions" to True
Then when I view the videoalpha it does show the captions
#pylint: disable=C0111 #pylint: disable=C0111
from lettuce import world, step from lettuce import world, step
from terrain.steps import reload_the_page
############### ACTIONS #################### ############### ACTIONS ####################
@step('when I view the video it does not have autoplay enabled') @step('when I view the (.*) it does not have autoplay enabled')
def does_not_autoplay(_step): def does_not_autoplay(_step, video_type):
assert world.css_find('.video')[0]['data-autoplay'] == 'False' assert world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False'
assert world.css_has_class('.video_control', 'play') assert world.css_has_class('.video_control', 'play')
...@@ -31,3 +32,15 @@ def hide_or_show_captions(step, shown): ...@@ -31,3 +32,15 @@ def hide_or_show_captions(step, shown):
button = world.css_find(button_css) button = world.css_find(button_css)
button.mouse_out() button.mouse_out()
world.css_click(button_css) world.css_click(button_css)
@step('I edit the component')
def i_edit_the_component(_step):
world.edit_component()
@step('my videoalpha display name change is persisted on save')
def videoalpha_name_persisted(step):
world.css_click('a.save-button')
reload_the_page(step)
world.edit_component()
world.verify_setting_entry(world.get_setting_entry('Display Name'), 'Display Name', '3.4', True)
../../../templates/js/metadata-list-entry.underscore
\ No newline at end of file
...@@ -3,12 +3,14 @@ describe "Test Metadata Editor", -> ...@@ -3,12 +3,14 @@ describe "Test Metadata Editor", ->
numberEntryTemplate = readFixtures('metadata-number-entry.underscore') numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
stringEntryTemplate = readFixtures('metadata-string-entry.underscore') stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
optionEntryTemplate = readFixtures('metadata-option-entry.underscore') optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
listEntryTemplate = readFixtures('metadata-list-entry.underscore')
beforeEach -> beforeEach ->
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate)) setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate)) appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate)) appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate)) appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-list-entry", type: "text/template"}).text(listEntryTemplate))
genericEntry = { genericEntry = {
default_value: 'default value', default_value: 'default value',
...@@ -62,6 +64,18 @@ describe "Test Metadata Editor", -> ...@@ -62,6 +64,18 @@ describe "Test Metadata Editor", ->
value: 10.2 value: 10.2
} }
listEntry = {
default_value: ["a thing", "another thing"],
display_name: "List",
explicitly_set: false,
field_name: "list",
help: "A list of things.",
inheritable: false,
options: [],
type: CMS.Models.Metadata.LIST_TYPE,
value: ["the first display value", "the second"]
}
# Test for the editor that creates the individual views. # Test for the editor that creates the individual views.
describe "CMS.Views.Metadata.Editor creates editors for each field", -> describe "CMS.Views.Metadata.Editor creates editors for each field", ->
beforeEach -> beforeEach ->
...@@ -84,16 +98,18 @@ describe "Test Metadata Editor", -> ...@@ -84,16 +98,18 @@ describe "Test Metadata Editor", ->
{"display_name": "Never", "value": "never"}], {"display_name": "Never", "value": "never"}],
type: "unknown type", type: "unknown type",
value: null value: null
} },
listEntry
] ]
) )
it "creates child views on initialize, and sorts them alphabetically", -> it "creates child views on initialize, and sorts them alphabetically", ->
view = new CMS.Views.Metadata.Editor({collection: @model}) view = new CMS.Views.Metadata.Editor({collection: @model})
childModels = view.collection.models childModels = view.collection.models
expect(childModels.length).toBe(5) expect(childModels.length).toBe(6)
childViews = view.$el.find('.setting-input') # Be sure to check list view as well as other input types
expect(childViews.length).toBe(5) childViews = view.$el.find('.setting-input, .list-settings')
expect(childViews.length).toBe(6)
verifyEntry = (index, display_name, type) -> verifyEntry = (index, display_name, type) ->
expect(childModels[index].get('display_name')).toBe(display_name) expect(childModels[index].get('display_name')).toBe(display_name)
...@@ -101,9 +117,10 @@ describe "Test Metadata Editor", -> ...@@ -101,9 +117,10 @@ describe "Test Metadata Editor", ->
verifyEntry(0, 'Display Name', 'text') verifyEntry(0, 'Display Name', 'text')
verifyEntry(1, 'Inputs', 'number') verifyEntry(1, 'Inputs', 'number')
verifyEntry(2, 'Show Answer', 'select-one') verifyEntry(2, 'List', '')
verifyEntry(3, 'Unknown', 'text') verifyEntry(3, 'Show Answer', 'select-one')
verifyEntry(4, 'Weight', 'number') verifyEntry(4, 'Unknown', 'text')
verifyEntry(5, 'Weight', 'number')
it "returns its display name", -> it "returns its display name", ->
view = new CMS.Views.Metadata.Editor({collection: @model}) view = new CMS.Views.Metadata.Editor({collection: @model})
...@@ -146,27 +163,27 @@ describe "Test Metadata Editor", -> ...@@ -146,27 +163,27 @@ describe "Test Metadata Editor", ->
# Tests for individual views. # Tests for individual views.
assertInputType = (view, expectedType) -> assertInputType = (view, expectedType) ->
input = view.$el.find('.setting-input') input = view.$el.find('.setting-input')
expect(input.length).toBe(1) expect(input.length).toEqual(1)
expect(input[0].type).toBe(expectedType) expect(input[0].type).toEqual(expectedType)
assertValueInView = (view, expectedValue) -> assertValueInView = (view, expectedValue) ->
expect(view.getValueFromEditor()).toBe(expectedValue) expect(view.getValueFromEditor()).toEqual(expectedValue)
assertCanUpdateView = (view, newValue) -> assertCanUpdateView = (view, newValue) ->
view.setValueInEditor(newValue) view.setValueInEditor(newValue)
expect(view.getValueFromEditor()).toBe(newValue) expect(view.getValueFromEditor()).toEqual(newValue)
assertClear = (view, modelValue, editorValue=modelValue) -> assertClear = (view, modelValue, editorValue=modelValue) ->
view.clear() view.clear()
expect(view.model.getValue()).toBe(null) expect(view.model.getValue()).toBe(null)
expect(view.model.getDisplayValue()).toBe(modelValue) expect(view.model.getDisplayValue()).toEqual(modelValue)
expect(view.getValueFromEditor()).toBe(editorValue) expect(view.getValueFromEditor()).toEqual(editorValue)
assertUpdateModel = (view, originalValue, newValue) -> assertUpdateModel = (view, originalValue, newValue) ->
view.setValueInEditor(newValue) view.setValueInEditor(newValue)
expect(view.model.getValue()).toBe(originalValue) expect(view.model.getValue()).toEqual(originalValue)
view.updateModel() view.updateModel()
expect(view.model.getValue()).toBe(newValue) expect(view.model.getValue()).toEqual(newValue)
describe "CMS.Views.Metadata.String is a basic string input with clear functionality", -> describe "CMS.Views.Metadata.String is a basic string input with clear functionality", ->
beforeEach -> beforeEach ->
...@@ -298,3 +315,45 @@ describe "Test Metadata Editor", -> ...@@ -298,3 +315,45 @@ describe "Test Metadata Editor", ->
verifyDisallowedChars(@integerView) verifyDisallowedChars(@integerView)
verifyDisallowedChars(@floatView) verifyDisallowedChars(@floatView)
describe "CMS.Views.Metadata.List allows the user to enter an ordered list of strings", ->
beforeEach ->
listModel = new CMS.Models.Metadata(listEntry)
@listView = new CMS.Views.Metadata.List({model: listModel})
@el = @listView.$el
it "returns the initial value upon initialization", ->
assertValueInView(@listView, ['the first display value', 'the second'])
it "updates its value correctly", ->
assertCanUpdateView(@listView, ['a new item', 'another new item', 'a third'])
it "has a clear method to revert to the model default", ->
assertClear(@listView, ['a thing', 'another thing'])
it "has an update model method", ->
assertUpdateModel(@listView, null, ['a new value'])
it "can add an entry", ->
expect(@listView.model.get('value').length).toEqual(2)
@el.find('.create-setting').click()
expect(@el.find('input.input').length).toEqual(3)
it "can remove an entry", ->
expect(@listView.model.get('value').length).toEqual(2)
@el.find('.remove-setting').first().click()
expect(@listView.model.get('value').length).toEqual(1)
it "only allows one blank entry at a time", ->
expect(@el.find('input').length).toEqual(2)
@el.find('.create-setting').click()
@el.find('.create-setting').click()
expect(@el.find('input').length).toEqual(3)
it "re-enables the add setting button after entering a new value", ->
expect(@el.find('input').length).toEqual(2)
@el.find('.create-setting').click()
expect(@el.find('.create-setting')).toHaveClass('is-disabled')
@el.find('input').last().val('third setting')
@el.find('input').last().trigger('input')
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
...@@ -111,3 +111,4 @@ CMS.Models.Metadata.SELECT_TYPE = "Select"; ...@@ -111,3 +111,4 @@ CMS.Models.Metadata.SELECT_TYPE = "Select";
CMS.Models.Metadata.INTEGER_TYPE = "Integer"; CMS.Models.Metadata.INTEGER_TYPE = "Integer";
CMS.Models.Metadata.FLOAT_TYPE = "Float"; CMS.Models.Metadata.FLOAT_TYPE = "Float";
CMS.Models.Metadata.GENERIC_TYPE = "Generic"; CMS.Models.Metadata.GENERIC_TYPE = "Generic";
CMS.Models.Metadata.LIST_TYPE = "List";
...@@ -27,6 +27,9 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({ ...@@ -27,6 +27,9 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({
model.getType() === CMS.Models.Metadata.FLOAT_TYPE) { model.getType() === CMS.Models.Metadata.FLOAT_TYPE) {
new CMS.Views.Metadata.Number(data); new CMS.Views.Metadata.Number(data);
} }
else if(model.getType() === CMS.Models.Metadata.LIST_TYPE) {
new CMS.Views.Metadata.List(data);
}
else { else {
// Everything else is treated as GENERIC_TYPE, which uses String editor. // Everything else is treated as GENERIC_TYPE, which uses String editor.
new CMS.Views.Metadata.String(data); new CMS.Views.Metadata.String(data);
...@@ -310,3 +313,59 @@ CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({ ...@@ -310,3 +313,59 @@ CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
}).prop('selected', true); }).prop('selected', true);
} }
}); });
CMS.Views.Metadata.List = CMS.Views.Metadata.AbstractEditor.extend({
events : {
"click .setting-clear" : "clear",
"keypress .setting-input" : "showClearButton",
"change input" : "updateModel",
"input input" : "enableAdd",
"click .create-setting" : "addEntry",
"click .remove-setting" : "removeEntry"
},
templateName: "metadata-list-entry",
getValueFromEditor: function () {
return _.map(
this.$el.find('li input'),
function (ele) { return ele.value.trim(); }
).filter(_.identity);
},
setValueInEditor: function (value) {
var list = this.$el.find('ol');
list.empty();
_.each(value, function(ele, index) {
var template = _.template(
'<li class="list-settings-item">' +
'<input type="text" class="input" value="<%= ele %>">' +
'<a href="#" class="remove-action remove-setting" data-index="<%= index %>"><i class="icon-remove-sign"></i><span class="sr">Remove</span></a>' +
'</li>'
);
list.append($(template({'ele': ele, 'index': index})));
});
},
addEntry: function(event) {
event.preventDefault();
// We don't call updateModel here since it's bound to the
// change event
var list = this.model.get('value') || [];
this.setValueInEditor(list.concat(['']))
this.$el.find('.create-setting').addClass('is-disabled');
},
removeEntry: function(event) {
event.preventDefault();
var entry = $(event.currentTarget).siblings().val();
this.setValueInEditor(_.without(this.model.get('value'), entry));
this.updateModel();
this.$el.find('.create-setting').removeClass('is-disabled');
},
enableAdd: function() {
this.$el.find('.create-setting').removeClass('is-disabled');
}
});
...@@ -148,6 +148,14 @@ body.course.textbooks { ...@@ -148,6 +148,14 @@ body.course.textbooks {
padding: ($baseline*0.75) $baseline; padding: ($baseline*0.75) $baseline;
background: $gray-l6; background: $gray-l6;
.action {
margin-right: ($baseline/4);
&:last-child {
margin-right: 0;
}
}
// add a chapter is below with chapters styling // add a chapter is below with chapters styling
.action-primary { .action-primary {
......
...@@ -449,12 +449,39 @@ body.course.unit { ...@@ -449,12 +449,39 @@ body.course.unit {
// Module Actions, also used for Static Pages // Module Actions, also used for Static Pages
.module-actions { .module-actions {
box-shadow: inset 0 1px 1px $shadow; box-shadow: inset 0 1px 2px $shadow;
padding: 0 0 $baseline $baseline; border-top: 1px solid $gray-l1;
background-color: $gray-l6; padding: ($baseline*0.75) $baseline;
background: $gray-l6;
.action {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/4);
&:last-child {
margin-right: 0;
}
}
.action-primary {
@include blue-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
}
.save-button { .action-secondary {
margin: ($baseline/2) 8px 0 0; @include grey-button;
@extend .t-action2;
@include transition(all .15s);
display: inline-block;
padding: ($baseline/5) $baseline;
font-weight: 600;
text-transform: uppercase;
} }
} }
} }
...@@ -599,26 +626,27 @@ body.course.unit { ...@@ -599,26 +626,27 @@ body.course.unit {
} }
} }
.wrapper-comp-setting{ .wrapper-comp-setting {
display: inline-block; display: inline-block;
min-width: 300px; min-width: 300px;
width: 45%; width: 55%;
top: 0; top: 0;
vertical-align: top; vertical-align: top;
margin-bottom:5px; margin-bottom:5px;
position: relative; position: relative;
} }
label.setting-label { .setting-label {
@extend .t-copy-sub1; @extend .t-copy-sub1;
@include transition(color $tmg-f2 ease-in-out 0s); @include transition(color $tmg-f2 ease-in-out 0s);
font-weight: 400;
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
position: relative; position: relative;
left: 0; left: 0;
width: 33%;
min-width: 100px; min-width: 100px;
width: 35%; margin-right: ($baseline/2);
font-weight: 600;
&.is-focused { &.is-focused {
color: $blue; color: $blue;
...@@ -708,14 +736,98 @@ body.course.unit { ...@@ -708,14 +736,98 @@ body.course.unit {
} }
} }
.tip.setting-help { .setting-help {
@include font-size(12); @include font-size(12);
display: inline-block; display: inline-block;
font-color: $gray-l6; font-color: $gray-l6;
min-width: 260px; min-width: ($baseline*10);
width: 50%; width: 35%;
vertical-align: top; vertical-align: top;
} }
// TYPE: enumerated lists of metadata sets
.metadata-list-enum {
* {
@include box-sizing(border-box);
}
// label
.setting-label {
vertical-align: top;
margin-top: ($baseline/2);
}
// inputs and labels
.wrapper-list-settings {
@include size(45%,100%);
display: inline-block;
min-width: ($baseline*5);
// enumerated fields
.list-settings {
margin: 0;
.list-settings-item {
margin-bottom: ($baseline/2);
}
// inputs
.input {
width: 80%;
margin-right: ($baseline/2);
vertical-align: middle;
}
}
}
// actions
.create-action, .remove-action, .setting-clear {
}
.setting-clear {
vertical-align: top;
margin-top: ($baseline/4);
}
.create-setting {
@extend .ui-btn-flat-outline;
@extend .t-action3;
display: block;
width: 100%;
padding: ($baseline/2);
font-weight: 600;
*[class^="icon-"] {
margin-right: ($baseline/4);
}
// STATE: disabled
&.is-disabled {
}
}
.remove-setting {
@include transition(color 0.25s ease-in-out);
@include font-size(20);
display: inline-block;
background: transparent;
color: $blue-l3;
&:hover {
color: $blue;
}
// STATE: disabled
&.is-disabled {
}
}
}
} }
} }
} }
......
...@@ -26,8 +26,8 @@ ...@@ -26,8 +26,8 @@
</div> </div>
</div> </div>
<div class="row module-actions"> <div class="row module-actions">
<a href="#" class="save-button">${_("Save")}</a> <a href="#" class="save-button action-primary action">${_("Save")}</a>
<a href="#" class="cancel-button">${_("Cancel")}</a> <a href="#" class="cancel-button action-secondary action">${_("Cancel")}</a>
</div> <!-- Module Actions--> </div> <!-- Module Actions-->
</div> </div>
</div> </div>
......
<ul class="list-input settings-list"> <ul class="list-input settings-list">
<% _.each(_.range(numEntries), function() { %> <% _.each(_.range(numEntries), function() { %>
<li class="field comp-setting-entry metadata_entry" id="settings-listing"> <li class="field comp-setting-entry metadata_entry">
</li> </li>
<% }) %> <% }) %>
</ul> </ul>
<div class="wrapper-comp-setting metadata-list-enum">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name')%></label>
<div id="<%= uniqueId %>" class="wrapper-list-settings">
<ol class="list-settings">
</ol>
<a href="#" class="create-action create-setting">
<i class="icon-plus"></i><%= gettext("Add") %> <span class="sr"><%= model.get('display_name')%></span>
</a>
</div>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i>
<span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
...@@ -25,6 +25,10 @@ ...@@ -25,6 +25,10 @@
<%static:include path="js/metadata-option-entry.underscore" /> <%static:include path="js/metadata-option-entry.underscore" />
</script> </script>
<script id="metadata-list-entry" type="text/template">
<%static:include path="js/metadata-list-entry.underscore" />
</script>
<% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %> <% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
<% metadata_field_copy = copy.copy(editable_metadata_fields) %> <% metadata_field_copy = copy.copy(editable_metadata_fields) %>
## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it. ## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
...@@ -40,4 +44,4 @@ ...@@ -40,4 +44,4 @@
<%include file="source-edit.html" /> <%include file="source-edit.html" />
% endif % endif
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy) | h}'/> <div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy) | h}'/>
\ No newline at end of file
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
% endfor % endfor
</ul> </ul>
</div> </div>
<div class="${'tabs-wrapper' if (len(tabs) != 1) else 'editor-single-tab' }"> <div class="tabs-wrapper">
% for tab in tabs: % for tab in tabs:
<div class="component-tab ${'is-inactive' if not tab.get('current', False) else ''}" id="tab-${html_id}-${loop.index}" > <div class="component-tab ${'is-inactive' if not tab.get('current', False) else ''}" id="tab-${html_id}-${loop.index}" >
<%include file="${tab['template']}" args="tabName=tab['name']"/> <%include file="${tab['template']}" args="tabName=tab['name']"/>
......
...@@ -20,5 +20,9 @@ ...@@ -20,5 +20,9 @@
<%static:include path="js/metadata-option-entry.underscore" /> <%static:include path="js/metadata-option-entry.underscore" />
</script> </script>
<script id="metadata-list-entry" type="text/template">
<%static:include path="js/metadata-list-entry.underscore" />
</script>
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(editable_metadata_fields) | h}'/> <div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(editable_metadata_fields) | h}'/>
...@@ -148,7 +148,7 @@ function (VideoPlayer) { ...@@ -148,7 +148,7 @@ function (VideoPlayer) {
// Option // Option
// this.config.show_captions = true | false // this.config.show_captions = true | false
// //
// defines whether to turn off/on the captions altogether. User will not have the ability to turn them on/off. // Defines whether or not captions are shown on first viewing.
// //
// Option // Option
// this.hide_captions = true | false // this.hide_captions = true | false
......
...@@ -60,10 +60,7 @@ function ( ...@@ -60,10 +60,7 @@ function (
VideoProgressSlider(state); VideoProgressSlider(state);
VideoVolumeControl(state); VideoVolumeControl(state);
VideoSpeedControl(state); VideoSpeedControl(state);
VideoCaption(state);
if (state.config.show_captions) {
VideoCaption(state);
}
// Because the 'state' object is only available inside this closure, we will also make // Because the 'state' object is only available inside this closure, we will also make
// it available to the caller by returning it. This is necessary so that we can test // it available to the caller by returning it. This is necessary so that we can test
......
...@@ -39,7 +39,8 @@ class TestFields(object): ...@@ -39,7 +39,8 @@ class TestFields(object):
float_non_select = Float(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3}) float_non_select = Float(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
# Used for testing that Booleans get mapped to select type # Used for testing that Booleans get mapped to select type
boolean_select = Boolean(scope=Scope.settings) boolean_select = Boolean(scope=Scope.settings)
# Used for testing Lists
list_field = List(scope=Scope.settings, default=[])
class EditableMetadataFieldsTest(unittest.TestCase): class EditableMetadataFieldsTest(unittest.TestCase):
def test_display_name_field(self): def test_display_name_field(self):
...@@ -63,7 +64,7 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -63,7 +64,7 @@ class EditableMetadataFieldsTest(unittest.TestCase):
def test_integer_field(self): def test_integer_field(self):
descriptor = self.get_descriptor({'max_attempts': '7'}) descriptor = self.get_descriptor({'max_attempts': '7'})
editable_fields = descriptor.editable_metadata_fields editable_fields = descriptor.editable_metadata_fields
self.assertEqual(6, len(editable_fields)) self.assertEqual(7, len(editable_fields))
self.assert_field_values( self.assert_field_values(
editable_fields, 'max_attempts', TestFields.max_attempts, editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer', explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer',
...@@ -137,6 +138,12 @@ class EditableMetadataFieldsTest(unittest.TestCase): ...@@ -137,6 +138,12 @@ class EditableMetadataFieldsTest(unittest.TestCase):
type='Float', options={'min': 0, 'step': .3} type='Float', options={'min': 0, 'step': .3}
) )
self.assert_field_values(
editable_fields, 'list_field', TestFields.list_field,
explicitly_set=False, inheritable=False, value=[], default_value=[],
type='List'
)
# Start of helper methods # Start of helper methods
def get_xml_editable_fields(self, model_data): def get_xml_editable_fields(self, model_data):
system = get_test_system() system = get_test_system()
......
...@@ -10,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir ...@@ -10,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import inheritance, Location from xmodule.modulestore import inheritance, Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError, InvalidLocationError
from xblock.core import XBlock, Scope, String, Integer, Float, ModelType from xblock.core import XBlock, Scope, String, Integer, Float, List, ModelType
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblock.runtime import Runtime from xblock.runtime import Runtime
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
...@@ -766,7 +766,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -766,7 +766,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# 2. Number editors for integers and floats. # 2. Number editors for integers and floats.
# 3. A generic string editor for anything else (editing JSON representation of the value). # 3. A generic string editor for anything else (editing JSON representation of the value).
editor_type = "Generic" editor_type = "Generic"
values = [] if field.values is None else copy.deepcopy(field.values) values = copy.deepcopy(field.values)
if isinstance(values, tuple): if isinstance(values, tuple):
values = list(values) values = list(values)
if isinstance(values, list): if isinstance(values, list):
...@@ -783,11 +783,13 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -783,11 +783,13 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
editor_type = "Integer" editor_type = "Integer"
elif isinstance(field, Float): elif isinstance(field, Float):
editor_type = "Float" editor_type = "Float"
elif isinstance(field, List):
editor_type = "List"
metadata_fields[field.name] = {'field_name': field.name, metadata_fields[field.name] = {'field_name': field.name,
'type': editor_type, 'type': editor_type,
'display_name': field.display_name, 'display_name': field.display_name,
'value': field.to_json(value), 'value': field.to_json(value),
'options': values, 'options': [] if values is None else values,
'default_value': field.to_json(default_value), 'default_value': field.to_json(default_value),
'inheritable': inheritable, 'inheritable': inheritable,
'explicitly_set': explicitly_set, 'explicitly_set': explicitly_set,
......
...@@ -81,12 +81,16 @@ class BaseTestXmodule(ModuleStoreTestCase): ...@@ -81,12 +81,16 @@ class BaseTestXmodule(ModuleStoreTestCase):
# Allow us to assert that the template was called in the same way from # Allow us to assert that the template was called in the same way from
# different code paths while maintaining the type returned by render_template # different code paths while maintaining the type returned by render_template
self.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items())) self.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items()))
model_data = {'location': self.item_descriptor.location} model_data = {'location': self.item_descriptor.location}
model_data.update(self.MODEL_DATA) model_data.update(self.MODEL_DATA)
self.item_module = self.item_descriptor.module_class( self.item_module = self.item_descriptor.module_class(
self.runtime, self.item_descriptor, model_data self.runtime,
self.item_descriptor,
model_data
) )
self.item_url = Location(self.item_module.location).url() self.item_url = Location(self.item_module.location).url()
# login all users for acces to Xmodule # login all users for acces to Xmodule
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
from . import BaseTestXmodule from . import BaseTestXmodule
from .test_videoalpha_xml import SOURCE_XML from .test_videoalpha_xml import SOURCE_XML
from django.conf import settings from django.conf import settings
from xmodule.videoalpha_module import _create_youtube_string
class TestVideo(BaseTestXmodule): class TestVideo(BaseTestXmodule):
...@@ -15,6 +16,14 @@ class TestVideo(BaseTestXmodule): ...@@ -15,6 +16,14 @@ class TestVideo(BaseTestXmodule):
'data': DATA 'data': DATA
} }
def setUp(self):
# Since the VideoAlphaDescriptor changes `self._model_data`,
# we need to instantiate `self.item_module` through
# `self.item_descriptor` rather than directly constructing it
super(TestVideo, self).setUp()
self.item_module = self.item_descriptor.xmodule(self.runtime)
self.item_module.runtime.render_template = lambda template, context: context
def test_handle_ajax_dispatch(self): def test_handle_ajax_dispatch(self):
responses = { responses = {
user.username: self.clients[user.username].post( user.username: self.clients[user.username].post(
...@@ -34,22 +43,31 @@ class TestVideo(BaseTestXmodule): ...@@ -34,22 +43,31 @@ class TestVideo(BaseTestXmodule):
def test_videoalpha_constructor(self): def test_videoalpha_constructor(self):
"""Make sure that all parameters extracted correclty from xml""" """Make sure that all parameters extracted correclty from xml"""
fragment = self.runtime.render(self.item_module, None, 'student_view') context = self.item_module.get_html()
sources = {
'main': 'example.mp4',
'mp4': 'example.mp4',
'webm': 'example.webm',
'ogv': 'example.ogv'
}
expected_context = { expected_context = {
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_', 'caption_asset_path': '/c4x/MITx/999/asset/subs_',
'show_captions': self.item_module.show_captions, 'show_captions': 'true',
'display_name': self.item_module.display_name_with_default, 'display_name': 'A Name',
'end': self.item_module.end_time, 'end': 3610.0,
'id': self.item_module.location.html_id(), 'id': self.item_module.location.html_id(),
'sources': self.item_module.sources, 'sources': sources,
'start': self.item_module.start_time, 'start': 3603.0,
'sub': self.item_module.sub, 'sub': 'a_sub_file.srt.sjson',
'track': self.item_module.track, 'track': '',
'youtube_streams': self.item_module.youtube_streams, 'youtube_streams': _create_youtube_string(self.item_module),
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
} }
self.assertEqual(fragment.content, self.runtime.render_template('videoalpha.html', expected_context))
self.assertEqual(context, expected_context)
class TestVideoNonYouTube(TestVideo): class TestVideoNonYouTube(TestVideo):
...@@ -57,14 +75,13 @@ class TestVideoNonYouTube(TestVideo): ...@@ -57,14 +75,13 @@ class TestVideoNonYouTube(TestVideo):
DATA = """ DATA = """
<videoalpha show_captions="true" <videoalpha show_captions="true"
data_dir="" display_name="A Name"
caption_asset_path="" sub="a_sub_file.srt.sjson"
autoplay="true"
start_time="01:00:03" end_time="01:00:10" start_time="01:00:03" end_time="01:00:10"
> >
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/> <source src="example.mp4"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/> <source src="example.webm"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/> <source src="example.ogv"/>
</videoalpha> </videoalpha>
""" """
MODEL_DATA = { MODEL_DATA = {
...@@ -75,20 +92,28 @@ class TestVideoNonYouTube(TestVideo): ...@@ -75,20 +92,28 @@ class TestVideoNonYouTube(TestVideo):
"""Make sure that if the 'youtube' attribute is omitted in XML, then """Make sure that if the 'youtube' attribute is omitted in XML, then
the template generates an empty string for the YouTube streams. the template generates an empty string for the YouTube streams.
""" """
sources = {
u'main': u'example.mp4',
u'mp4': u'example.mp4',
u'webm': u'example.webm',
u'ogv': u'example.ogv'
}
context = self.item_module.get_html()
fragment = self.runtime.render(self.item_module, None, 'student_view')
expected_context = { expected_context = {
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'caption_asset_path': '/c4x/MITx/999/asset/subs_', 'caption_asset_path': '/c4x/MITx/999/asset/subs_',
'show_captions': self.item_module.show_captions, 'show_captions': 'true',
'display_name': self.item_module.display_name_with_default, 'display_name': 'A Name',
'end': self.item_module.end_time, 'end': 3610.0,
'id': self.item_module.location.html_id(), 'id': self.item_module.location.html_id(),
'sources': self.item_module.sources, 'sources': sources,
'start': self.item_module.start_time, 'start': 3603.0,
'sub': self.item_module.sub, 'sub': 'a_sub_file.srt.sjson',
'track': self.item_module.track, 'track': '',
'youtube_streams': '', 'youtube_streams': '1.00:OEoXaMPEzfM',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
} }
self.assertEqual(fragment.content, self.runtime.render_template('videoalpha.html', expected_context))
self.assertEqual(context, expected_context)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Test for VideoAlpha Xmodule functional logic. """Test for VideoAlpha Xmodule functional logic.
These tests data readed from xml, not from mongo. These test data read from xml, not from mongo.
We have a ModuleStoreTestCase class defined in We have a ModuleStoreTestCase class defined in
common/lib/xmodule/xmodule/modulestore/tests/django_utils.py. common/lib/xmodule/xmodule/modulestore/tests/django_utils.py.
...@@ -15,28 +15,24 @@ course, section, subsection, unit, etc. ...@@ -15,28 +15,24 @@ course, section, subsection, unit, etc.
import json import json
import unittest import unittest
from mock import Mock
from lxml import etree
from django.conf import settings from django.conf import settings
from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule from xmodule.videoalpha_module import VideoAlphaDescriptor, _create_youtube_string
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.tests import get_test_system from xmodule.tests import get_test_system
from xmodule.tests import LogicTest
SOURCE_XML = """ SOURCE_XML = """
<videoalpha show_captions="true" <videoalpha show_captions="true"
display_name="A Name"
youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg" youtube="0.75:jNCf2gIqpeE,1.0:ZwkTiUPN0mg,1.25:rsq9auxASqI,1.50:kMyNdzVHHgg"
data_dir="" sub="a_sub_file.srt.sjson"
caption_asset_path=""
autoplay="true"
start_time="01:00:03" end_time="01:00:10" start_time="01:00:03" end_time="01:00:10"
> >
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.mp4"/> <source src="example.mp4"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.webm"/> <source src="example.webm"/>
<source src=".../mit-3091x/M-3091X-FA12-L21-3_100.ogv"/> <source src="example.ogv"/>
</videoalpha> </videoalpha>
""" """
...@@ -54,74 +50,53 @@ class VideoAlphaFactory(object): ...@@ -54,74 +50,53 @@ class VideoAlphaFactory(object):
"""Method return VideoAlpha Xmodule instance.""" """Method return VideoAlpha Xmodule instance."""
location = Location(["i4x", "edX", "videoalpha", "default", location = Location(["i4x", "edX", "videoalpha", "default",
"SampleProblem1"]) "SampleProblem1"])
model_data = {'data': VideoAlphaFactory.sample_problem_xml_youtube} model_data = {'data': VideoAlphaFactory.sample_problem_xml_youtube,
'location': location}
descriptor = Mock(weight="1")
system = get_test_system() system = get_test_system()
system.render_template = lambda template, context: context system.render_template = lambda template, context: context
VideoAlphaModule.location = location
module = VideoAlphaModule(system, descriptor, model_data)
return module
class VideoAlphaModuleTest(LogicTest):
"""Tests for logic of VideoAlpha Xmodule."""
descriptor_class = VideoAlphaDescriptor
raw_model_data = {
'data': '<videoalpha />'
}
def test_get_timeframe_no_parameters(self): descriptor = VideoAlphaDescriptor(system, model_data)
xmltree = etree.fromstring('<videoalpha>test</videoalpha>')
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, ('', ''))
def test_get_timeframe_with_one_parameter(self): module = descriptor.xmodule(system)
xmltree = etree.fromstring(
'<videoalpha start_time="00:04:07">test</videoalpha>'
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, ''))
def test_get_timeframe_with_two_parameters(self): return module
xmltree = etree.fromstring(
'''<videoalpha
start_time="00:04:07"
end_time="13:04:39"
>test</videoalpha>'''
)
output = self.xmodule.get_timeframe(xmltree)
self.assertEqual(output, (247, 47079))
class VideoAlphaModuleUnitTest(unittest.TestCase): class VideoAlphaModuleUnitTest(unittest.TestCase):
"""Unit tests for VideoAlpha Xmodule.""" """Unit tests for VideoAlpha Xmodule."""
def test_videoalpha_constructor(self): def test_videoalpha_get_html(self):
"""Make sure that all parameters extracted correclty from xml""" """Make sure that all parameters extracted correclty from xml"""
module = VideoAlphaFactory.create() module = VideoAlphaFactory.create()
module.runtime.render_template = lambda template, context: u'{!r}, {!r}'.format(template, sorted(context.items())) module.runtime.render_template = lambda template, context: context
sources = {
'main': 'example.mp4',
'mp4': 'example.mp4',
'webm': 'example.webm',
'ogv': 'example.ogv'
}
fragment = module.runtime.render(module, None, 'student_view')
expected_context = { expected_context = {
'caption_asset_path': '/static/subs/', 'caption_asset_path': '/static/subs/',
'sub': module.sub, 'sub': 'a_sub_file.srt.sjson',
'data_dir': getattr(self, 'data_dir', None), 'data_dir': getattr(self, 'data_dir', None),
'display_name': module.display_name_with_default, 'display_name': 'A Name',
'end': module.end_time, 'end': 3610.0,
'start': module.start_time, 'start': 3603.0,
'id': module.location.html_id(), 'id': module.location.html_id(),
'show_captions': module.show_captions, 'show_captions': 'true',
'sources': module.sources, 'sources': sources,
'youtube_streams': module.youtube_streams, 'youtube_streams': _create_youtube_string(module),
'track': module.track, 'track': '',
'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True)
} }
self.assertEqual(fragment.content, module.runtime.render_template('videoalpha.html', expected_context))
self.assertEqual(module.get_html(), expected_context)
def test_videoalpha_instance_state(self):
module = VideoAlphaFactory.create()
self.assertDictEqual( self.assertDictEqual(
json.loads(module.get_instance_state()), json.loads(module.get_instance_state()),
......
...@@ -58,17 +58,13 @@ ...@@ -58,17 +58,13 @@
<a href="#" class="add-fullscreen" title="${_('Fill browser')}">${_('Fill browser')}</a> <a href="#" class="add-fullscreen" title="${_('Fill browser')}">${_('Fill browser')}</a>
<a href="#" class="quality_control" title="${_('HD')}">${_('HD')}</a> <a href="#" class="quality_control" title="${_('HD')}">${_('HD')}</a>
% if show_captions == 'true': <a href="#" class="hide-subtitles" title="${_('Turn off captions')}">Captions</a>
<a href="#" class="hide-subtitles" title="${_('Turn off captions')}">${_('Captions')}</a>
% endif
</div> </div>
</div> </div>
</section> </section>
</article> </article>
% if show_captions == 'true':
<ol class="subtitles"><li></li></ol> <ol class="subtitles"><li></li></ol>
% endif
</div> </div>
</div> </div>
......
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