Commit 74b81527 by Peter Fogg

Add a list view for metadata editor.

This is to be used with VideoAlpha's functionality allowing a choice
of multiple HTML5 video sources.
parent 4d5727a2
../../../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,17 @@ describe "Test Metadata Editor", -> ...@@ -84,16 +98,17 @@ 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') childViews = view.$el.find('.setting-input')
expect(childViews.length).toBe(5) 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 +116,10 @@ describe "Test Metadata Editor", -> ...@@ -101,9 +116,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', 'text')
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 +162,27 @@ describe "Test Metadata Editor", -> ...@@ -146,27 +162,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 +314,23 @@ describe "Test Metadata Editor", -> ...@@ -298,3 +314,23 @@ 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})
it "uses a text input type", ->
assertInputType(@listView, 'text')
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'])
...@@ -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,23 @@ CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({ ...@@ -310,3 +313,23 @@ 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"
},
templateName: "metadata-list-entry",
getValueFromEditor: function () {
return _.map(this.$el.find('#' + this.uniqueId).val().split(/,/),
function (url) { return url.trim(); });
},
setValueInEditor: function (value) {
this.$el.find('.input').val(value.join(', '));
}
});
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name')%></label>
<input class="input setting-input" type="text" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
<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
...@@ -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,
......
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