Commit faa97379 by polesye

BLD-658: Add view for field type Dict in Studio.

parent fdda638f
......@@ -5,6 +5,11 @@ 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.
Blades: Fix for the list metadata editor that gets into a bad state where "Add"
is disabled. BLD-821.
Blades: Add view for field type Dict in Studio. BLD-658.
Blades: Refactor stub implementation of LTI Provider. BLD-601.
LMS: In left accordion and progress page, due dates are now displayed in time
......
......@@ -14,6 +14,7 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
listEntryTemplate = readFixtures('metadata-list-entry.underscore')
dictEntryTemplate = readFixtures('metadata-dict-entry.underscore')
beforeEach ->
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
......@@ -21,6 +22,7 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c
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-list-entry", type: "text/template"}).text(listEntryTemplate))
appendSetFixtures($("<script>", {id: "metadata-dict-entry", type: "text/template"}).text(dictEntryTemplate))
genericEntry = {
default_value: 'default value',
......@@ -92,6 +94,24 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c
value: "12:12:12"
}
dictEntry = {
default_value: {
'en': 'English',
'ru': 'Русский'
},
display_name: "New Dict",
explicitly_set: false,
field_name: "dict",
help: "Specifies the name for this component.",
type: MetadataModel.DICT_TYPE,
value: {
'en': 'English',
'ru': 'Русский',
'ua': 'Українська',
'fr': 'Français'
}
}
# Test for the editor that creates the individual views.
describe "MetadataView.Editor creates editors for each field", ->
......@@ -116,17 +136,18 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c
value: null
},
listEntry,
timeEntry
timeEntry,
dictEntry
]
)
it "creates child views on initialize, and sorts them alphabetically", ->
view = new MetadataView.Editor({collection: @model})
childModels = view.collection.models
expect(childModels.length).toBe(7)
expect(childModels.length).toBe(8)
# Be sure to check list view as well as other input types
childViews = view.$el.find('.setting-input, .list-settings')
expect(childViews.length).toBe(7)
expect(childViews.length).toBe(8)
verifyEntry = (index, display_name, type) ->
expect(childModels[index].get('display_name')).toBe(display_name)
......@@ -135,10 +156,11 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c
verifyEntry(0, 'Display Name', 'text')
verifyEntry(1, 'Inputs', 'number')
verifyEntry(2, 'List', '')
verifyEntry(3, 'Show Answer', 'select-one')
verifyEntry(4, 'Time', 'text')
verifyEntry(5, 'Unknown', 'text')
verifyEntry(6, 'Weight', 'number')
verifyEntry(3, 'New Dict', '')
verifyEntry(4, 'Show Answer', 'select-one')
verifyEntry(5, 'Time', 'text')
verifyEntry(6, 'Unknown', 'text')
verifyEntry(7, 'Weight', 'number')
it "returns its display name", ->
view = new MetadataView.Editor({collection: @model})
......@@ -351,7 +373,9 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c
assertCanUpdateView(@listView, ['a new item', 'another new item', 'a third'])
it "has a clear method to revert to the model default", ->
@el.find('.create-setting').click()
assertClear(@listView, ['a thing', 'another thing'])
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
it "has an update model method", ->
assertUpdateModel(@listView, null, ['a new value'])
......@@ -486,3 +510,99 @@ define ["js/models/metadata", "js/collections/metadata", "js/views/metadata", "c
it "has an update model method", ->
assertUpdateModel(@view, '12:12:12', '23:59:59')
describe "MetadataView.Dict allows the user to enter key-value pairs of strings", ->
beforeEach ->
dictModel = new MetadataModel($.extend(true, {}, dictEntry))
@dictView = new MetadataView.Dict({model: dictModel})
@el = @dictView.$el
main()
it "returns the initial value upon initialization", ->
assertValueInView(@dictView, {
'en': 'English',
'ru': 'Русский',
'ua': 'Українська',
'fr': 'Français'
})
it "updates its value correctly", ->
assertCanUpdateView(@dictView, {
'ru': 'Русский',
'ua': 'Українська',
'fr': 'Français'
})
it "has a clear method to revert to the model default", ->
@el.find('.create-setting').click()
assertClear(@dictView, {
'en': 'English',
'ru': 'Русский'
})
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
it "has an update model method", ->
assertUpdateModel(@dictView, null, {'fr': 'Français'})
it "can add an entry", ->
expect(_.keys(@dictView.model.get('value')).length).toEqual(4)
@el.find('.create-setting').click()
expect(@el.find('input.input-key').length).toEqual(5)
it "can remove an entry", ->
expect(_.keys(@dictView.model.get('value')).length).toEqual(4)
@el.find('.remove-setting').first().click()
expect(_.keys(@dictView.model.get('value')).length).toEqual(3)
it "only allows one blank entry at a time", ->
expect(@el.find('input.input-key').length).toEqual(4)
@el.find('.create-setting').click()
@el.find('.create-setting').click()
expect(@el.find('input.input-key').length).toEqual(5)
it "only allows unique keys", ->
data = [
{
expectedValue: {'ru': 'Русский'},
initialValue: {'ru': 'Русский'},
testValue: {
'key': 'ru'
'value': ''
}
},
{
expectedValue: {'ru': 'Русский'},
initialValue: {'ru': 'Some value'},
testValue: {
'key': 'ru'
'value': 'Русский'
}
},
{
expectedValue: {'ru': 'Русский'},
initialValue: {'ru': 'Русский'},
testValue: {
'key': ''
'value': ''
}
}
]
_.each data, ((d, index) ->
@dictView.setValueInEditor(d.initialValue)
@dictView.updateModel();
@el.find('.create-setting').click()
item = @el.find('.list-settings-item').last()
item.find('.input-key').val(d.testValue.key);
item.find('.input-value').val(d.testValue.value);
expect(@dictView.getValueFromEditor()).toEqual(d.expectedValue)
).bind(@)
it "re-enables the add setting button after entering a new value", ->
expect(@el.find('input.input-key').length).toEqual(4)
@el.find('.create-setting').click()
expect(@el.find('.create-setting')).toHaveClass('is-disabled')
@el.find('input.input-key').last().val('third setting')
@el.find('input.input-key').last().trigger('input')
expect(@el.find('.create-setting')).not.toHaveClass('is-disabled')
......@@ -107,6 +107,7 @@ define(["backbone"], function(Backbone) {
Metadata.FLOAT_TYPE = "Float";
Metadata.GENERIC_TYPE = "Generic";
Metadata.LIST_TYPE = "List";
Metadata.DICT_TYPE = "Dict";
Metadata.VIDEO_LIST_TYPE = "VideoList";
Metadata.RELATIVE_TIME_TYPE = "RelativeTime";
......
......@@ -23,26 +23,23 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
this.collection.each(
function (model) {
var data = {
el: self.$el.find('.metadata_entry')[counter++],
model: model
};
if (model.getType() === MetadataModel.SELECT_TYPE) {
new Metadata.Option(data);
el: self.$el.find('.metadata_entry')[counter++],
model: model
},
conversions = {
'Select': 'Option',
'Float': 'Number',
'Integer': 'Number'
},
type = model.getType();
if (conversions[type]) {
type = conversions[type];
}
else if (model.getType() === MetadataModel.INTEGER_TYPE ||
model.getType() === MetadataModel.FLOAT_TYPE) {
new Metadata.Number(data);
}
else if(model.getType() === MetadataModel.LIST_TYPE) {
new Metadata.List(data);
}
else if(model.getType() === MetadataModel.VIDEO_LIST_TYPE) {
new VideoList(data);
}
else if(model.getType() === MetadataModel.RELATIVE_TIME_TYPE) {
new Metadata.RelativeTime(data);
}
else {
if (_.isFunction(Metadata[type])) {
new Metadata[type](data);
} else {
// Everything else is treated as GENERIC_TYPE, which uses String editor.
new Metadata.String(data);
}
......@@ -84,6 +81,8 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
}
});
Metadata.VideoList = VideoList;
Metadata.String = AbstractEditor.extend({
events : {
......@@ -277,6 +276,7 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
setValueInEditor: function (value) {
var list = this.$el.find('ol');
list.empty();
_.each(value, function(ele, index) {
var template = _.template(
......@@ -308,6 +308,13 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
enableAdd: function() {
this.$el.find('.create-setting').removeClass('is-disabled');
},
clear: function() {
AbstractEditor.prototype.clear.apply(this, arguments);
if (_.isNull(this.model.getValue())) {
this.$el.find('.create-setting').removeClass('is-disabled');
}
}
});
......@@ -386,5 +393,89 @@ function(BaseView, _, MetadataModel, AbstractEditor, VideoList) {
}
});
Metadata.Dict = 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-dict-entry",
getValueFromEditor: function () {
var dict = {};
_.each(this.$el.find('li'), function(li, index) {
var key = $(li).find('.input-key').val().trim(),
value = $(li).find('.input-value').val().trim();
// Keys should be unique, so if our keys are duplicated and
// second key is empty or key and value are empty just do
// nothing. Otherwise, it'll be overwritten by the new value.
if (value === '') {
if (key === '' || key in dict) {
return false;
}
}
dict[key] = value;
});
return dict;
},
setValueInEditor: function (value) {
var list = this.$el.find('ol'),
frag = document.createDocumentFragment();
_.each(value, function(value, key) {
var template = _.template(
'<li class="list-settings-item">' +
'<input type="text" class="input input-key" value="<%= key %>">' +
'<input type="text" class="input input-value" value="<%= value %>">' +
'<a href="#" class="remove-action remove-setting" data-value="<%= value %>"><i class="icon-remove-sign"></i><span class="sr">Remove</span></a>' +
'</li>'
);
frag.appendChild($(template({'key': key, 'value': value}))[0]);
});
list.html([frag]);
},
addEntry: function(event) {
event.preventDefault();
// We don't call updateModel here since it's bound to the
// change event
var dict = $.extend(true, {}, this.model.get('value')) || {};
dict[''] = '';
this.setValueInEditor(dict);
this.$el.find('.create-setting').addClass('is-disabled');
},
removeEntry: function(event) {
event.preventDefault();
var entry = $(event.currentTarget).siblings('.input-key').val();
this.setValueInEditor(_.omit(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');
},
clear: function() {
AbstractEditor.prototype.clear.apply(this, arguments);
if (_.isNull(this.model.getValue())) {
this.$el.find('.create-setting').removeClass('is-disabled');
}
}
});
return Metadata;
});
......@@ -619,6 +619,7 @@ body.course.unit,.view-unit {
//component-setting-entry
.field.comp-setting-entry {
@include transition(opacity $tmg-f2 ease-in-out 0s);
background-color: $white;
padding: $baseline;
border-bottom: 1px solid $gray-l2;
......@@ -640,7 +641,6 @@ body.course.unit,.view-unit {
}
&:hover {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 1.0;
}
......@@ -654,9 +654,7 @@ body.course.unit,.view-unit {
}
.wrapper-comp-setting {
display: inline-block;
min-width: 300px;
width: 55%;
top: 0;
vertical-align: top;
margin-bottom:5px;
......@@ -670,7 +668,7 @@ body.course.unit,.view-unit {
display: inline-block;
position: relative;
left: 0;
width: 33%;
width: 25%;
min-width: 100px;
margin-right: ($baseline/2);
font-weight: 600;
......@@ -774,7 +772,6 @@ body.course.unit,.view-unit {
display: inline-block;
font-color: $gray-l6;
min-width: ($baseline*10);
width: 35%;
vertical-align: top;
}
......@@ -861,6 +858,77 @@ body.course.unit,.view-unit {
}
}
}
// TYPE: Dict
.metadata-dict {
* {
@include box-sizing(border-box);
}
// label
.setting-label {
vertical-align: top;
margin-top: ($baseline*.75);
}
// inputs and labels
.wrapper-dict-settings {
width: 55%;
display: inline-block;
min-width: 240px;
// enumerated fields
.list-settings {
margin: ($baseline/2) 0 0;
.list-settings-item {
margin-bottom: ($baseline/2);
}
// inputs
.input {
width: 43%;
margin-right: ($baseline/4);
vertical-align: middle;
display: inline-block;
&.input-value {
margin-right: ($baseline/2);
}
}
}
}
.setting-clear {
vertical-align: top;
margin: ($baseline*.75) 0 0 0;
}
.create-setting {
@extend %ui-btn-flat-outline;
@extend %t-action3;
display: block;
width: 88%;
padding: ($baseline/2);
font-weight: 600;
*[class^="icon-"] {
margin-right: ($baseline/4);
}
}
.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;
}
}
}
}
}
}
......
<div class="wrapper-comp-setting metadata-dict">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name')%></label>
<div id="<%= uniqueId %>" class="wrapper-dict-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>
......@@ -13,21 +13,11 @@
<%static:include path="js/metadata-editor.underscore" />
</script>
<script id="metadata-number-entry" type="text/template">
<%static:include path="js/metadata-number-entry.underscore" />
</script>
<script id="metadata-string-entry" type="text/template">
<%static:include path="js/metadata-string-entry.underscore" />
</script>
<script id="metadata-option-entry" type="text/template">
<%static:include path="js/metadata-option-entry.underscore" />
</script>
<script id="metadata-list-entry" type="text/template">
<%static:include path="js/metadata-list-entry.underscore" />
</script>
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"]:
<script id="${template_name}" type="text/template">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] and enable_latex_compiler %>
<% metadata_field_copy = copy.copy(editable_metadata_fields) %>
......
......@@ -8,21 +8,11 @@
<%static:include path="js/metadata-editor.underscore" />
</script>
<script id="metadata-number-entry" type="text/template">
<%static:include path="js/metadata-number-entry.underscore" />
</script>
<script id="metadata-string-entry" type="text/template">
<%static:include path="js/metadata-string-entry.underscore" />
</script>
<script id="metadata-option-entry" type="text/template">
<%static:include path="js/metadata-option-entry.underscore" />
</script>
<script id="metadata-list-entry" type="text/template">
<%static:include path="js/metadata-list-entry.underscore" />
</script>
% for template_name in ["metadata-number-entry", "metadata-string-entry", "metadata-option-entry", "metadata-list-entry", "metadata-dict-entry"]:
<script id="${template_name}" type="text/template">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(editable_metadata_fields) | h}'/>
......@@ -16,7 +16,7 @@ from webob import Response
from webob.multidict import MultiDict
from xblock.core import XBlock
from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String
from xblock.fields import Scope, Integer, Float, List, XBlockMixin, String, Dict
from xblock.fragment import Fragment
from xblock.plugin import default_select
from xblock.runtime import Runtime
......@@ -790,6 +790,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
editor_type = "Float"
elif isinstance(field, List):
editor_type = "List"
elif isinstance(field, Dict):
editor_type = "Dict"
elif isinstance(field, RelativeTime):
editor_type = "RelativeTime"
metadata_fields[field.name]['type'] = editor_type
......
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