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

This commit is contained in:
polesye
2014-02-05 19:22:43 +02:00
parent fdda638f57
commit faa97379b7
9 changed files with 343 additions and 62 deletions

View File

@@ -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')

View File

@@ -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";

View File

@@ -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;
});

View File

@@ -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;
}
}
}
}
}
}

View File

@@ -0,0 +1,14 @@
<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>

View File

@@ -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) %>

View File

@@ -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}'/>