Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 4 commits
  • 13 files changed
  • 0 commit comments
  • 1 contributor
Commits on May 09, 2013
@bruth bruth Fix missing reference to router options.id 521599d
@bruth bruth Clean up and simplify logic for context nodes
The `create` option on fetch now must be a node type of either
'branch', 'condition' or 'composite' to choose the correct constructor.

Fix missing call to root.save() prior to saving the ContextModel itself.
This prevented the publicAttributes object from every getting updated
before being sent to the server.

Add CONTEXT_SAVE channel to transparently request the session be saved.
2c5b09d
@bruth bruth Add option to hide field info for a single-field concept
This is prevent redundant information being rendered in the UI since
it is common to create single-field concepts with the same name and
description.
a40da28
@bruth bruth Remove unused main and chart regions
As of e38f66c ConceptForm does not render it's own chart, but
simply delegates to the first related field.
06bf078
View
1  coffee/cilantro/channels.coffee
@@ -44,6 +44,7 @@ define ->
CONTEXT_ADD: 'context.add'
CONTEXT_REMOVE: 'context.remove'
CONTEXT_CLEAR: 'context.clear'
+ CONTEXT_SAVE: 'context.save'
VIEW_CHANGED: 'view.changed'
VIEW_SYNCING: 'view.syncing'
View
191 coffee/cilantro/models/context.coffee
@@ -5,29 +5,16 @@ define [
class ContextNodeError extends Error
- queryAttrs = (attrs={}, query={}, options) ->
- if attrs instanceof ContextNodeModel
- attrs = attrs.attributes
-
- # No empty queries
- if c._.isEmpty(query) then return false
-
- # Check against each key in the query for a match on attrs
- for key, value of query
- if attrs[key] isnt value
- return false
-
- return true
-
-
# Represents a single node within a ContextModel tree. Non-branch nodes
# must be carefully handled and not removed from the tree unless explicitly
# removed
class ContextNodeModel extends c.Backbone.Model
- initialize: (attrs, options={}) ->
- # Save the initial state of the internal attributes
+
+ # Save of the default public attributes on initialization
+ initialize: (attrs, options) ->
@save(options)
+ # Returns the public attributes
toJSON: ->
@publicAttributes
@@ -39,6 +26,16 @@ define [
@publicAttributes = c._.clone @attributes
return isValid
+ # Clears all attributes except for the field and concept identifers
+ clear: (options) ->
+ attrs =
+ field: @get 'field'
+ concept: @get 'concept'
+ super(silent: true)
+ @set attrs, validate: false
+ return
+
+ # Validates the attributes are valid for the node type
validate: (attrs, options) ->
try
model = getContextNodeModel(attrs)
@@ -48,25 +45,46 @@ define [
return error.message
return
+ # Determines if the node is 'typed', that is, whether it is associated
+ # with a specific field or concept. If not, the node is considered a
+ # container for the attributes.
isTyped: ->
@attributes.field? or @attributes.concept?
+ # Attempts to fetch a node relative to this one. The `query` is a set
+ # of attributes the the target node must match in order to be
+ # returned. The current node is checked first and recurses (for branch
+ # nodes).
fetch: (query, options={}) ->
- if queryAttrs(@, query, options)
+ if c._.isEmpty(query) then return false
+
+ match = true
+
+ # Check against each key in the query for a match on attrs
+ for key, value of query
+ if @attributes[key] isnt value
+ match = false
+ break
+
+ if match
return @
- if options.create isnt false
- return new @constructor query
+ else if options.create
+ klass = contextNodeModels[options.create]
+ return new klass(query)
# Branch-type node that acts as a container for other nodes. The `type`
# determines the conditional relationship between the child nodes.
class BranchNodeModel extends ContextNodeModel
+ nodeType: 'branch'
+
defaults: ->
type: 'and'
children: []
- nodeType: 'branch'
-
+ # If `deep` is true, children are immediately converted into their
+ # respective context node instances. This is normally performed during
+ # a fetch.
initialize: (attrs, options) ->
options = c._.extend
deep: true
@@ -77,15 +95,24 @@ define [
for child, i in children
if not (child instanceof ContextNodeModel)
children[i] = getContextNodeModel(child, options)
+
super(attrs, options)
+ # If `deep` is true, children are also validated (and recursed). If
+ # any fail to validate, the branch is considered invalid. If `strict`
+ # is true, the branch must have at least one child to be valid
validate: (attrs, options) ->
+ options = c._.extend
+ deep: true
+ strict: false
+ , options
+
if not (attrs.type is 'and' or attrs.type is 'or')
- return 'Not a valid branch node'
+ return 'Not a valid branch type'
- options = c._.extend deep: true, options
+ if options.strict and not attrs.children.length
+ return 'No children in branch'
- # Recurse children and validate
if options.deep
for child in attrs.children
if child instanceof ContextNodeModel
@@ -95,30 +122,40 @@ define [
return message
return
+ # Attempts to fetch this node or one of the children based on the
+ # query attributes. To prevent pre-maturely creating a new node, the
+ # `create` option is explicity set to false during the recursion.
+ # One thing to note, is that a fetch does not uniformly increase it's
+ # depth of search per iteration. It will recurse as deep as it can go
+ # per child.
fetch: (query, options={}) ->
- # Set false to prevent shadowing the children below
create = options.create
options.create = false
+ # Initially check if this node matches
if (node = super(query, options))
return node
children = @get('children')
- # Recurse on each child node for a match, converting raw
- # attributes into nodes as needed
for child, i in children
if not (child instanceof ContextNodeModel)
child = children[i] = getContextNodeModel(child, options)
if (node = child.fetch(query, options))
return node
- if create isnt false
- @add(node = new @constructor query)
+ # No nodes matched, create a node of the specified type with the
+ # query as the default attributes.
+ if create
+ klass = contextNodeModels[create]
+ @add(node = new klass query)
return node
- # If this is a deep save, recursively save children prior to
- # creating a copy to publicAttributes.
+ # If `deep` is true, the save is recursed to each child. If `ignore`
+ # is true, child nodes that do validate will not be updated, but not
+ # be removed from the public attributes if already present, but their
+ # public attributes are not replaced. If `strict` is true, an invalid
+ # node will stop processing and return false
save: (options) ->
options = c._.extend
deep: true
@@ -126,44 +163,30 @@ define [
strict: false
, options
- if not super(deep: false) then return false
+ previousPublic = @publicAttributes
+
+ if not super(deep: false)
+ return false
+ # New attributes from super save
attrs = @publicAttributes
children = []
- # Recurse on children to ensure no node instances are present
for child in attrs.children
if child instanceof ContextNodeModel
- # Save child node if the deep option is passed. If strict
- # if true, any validation error will cause the save to fail
if options.deep
- if child.save(options)
- child = child.publicAttributes
- else
- if options.strict
- return false
- # Invalid children can be ignored (excluded from the array)
- # otherwise the previous state is maintained
- child = if options.ignore then null else child.publicAttributes
- else
- child = child.publicAttributes
-
- # Only if the child is not empty, append to the output
- if child? then children.push(child)
-
- attrs.children = children
- return true
-
- toJSON: ->
- attrs = super
- children = []
- # Recurse on children to ensure no node instances are present
- for child in attrs.children
- if child instanceof ContextNodeModel
+ if child.isValid(options)
+ child.save(options)
+ else if options.strict
+ @publicAttributes = previousPublic
+ return false
+ else if not options.ignore
+ continue
child = child.toJSON()
children.push(child)
+
attrs.children = children
- return attrs
+ return true
# Adds variable number of nodes to branch ensuring the same node
# is not added twice
@@ -195,25 +218,15 @@ define [
return @
clear: (options) ->
- options = c._.extend
- deep: true
- destroy: false
- , options
+ if not (children = @get('children')) or not children.length
+ return
- if options.deep or options.destroy
- children = []
-
- # Recurse on children to ensure no node instances are present
- for child in @get('children') or []
- if child instanceof ContextNodeModel
- if options.destroy
- child.destroy()
- else
- child.clear(options)
- children.push(child)
+ # Recurse on children and clear
+ for child in children
+ if child instanceof ContextNodeModel
+ child.clear(options)
- @set('children', children)
- return @
+ return
class ConditionNodeModel extends ContextNodeModel
@@ -232,15 +245,14 @@ define [
return 'Not a valid composite node'
- contextNodeModels = [
- BranchNodeModel
- ConditionNodeModel
- CompositeNodeModel
- ]
+ contextNodeModels =
+ branch: BranchNodeModel
+ condition: ConditionNodeModel
+ composite: CompositeNodeModel
# Returns the node model class appropriate for attrs
getContextNodeModel = (attrs, options) ->
- for model in contextNodeModels
+ for type, model of contextNodeModels
if not model::validate.call(null, attrs, options)
return new model(attrs, options)
throw new ContextNodeError 'Unknown context node type'
@@ -261,6 +273,9 @@ define [
children: []
super attrs, options
+ @on 'request', ->
+ c.publish c.CONTEXT_SYNCING, @
+
@on 'sync', ->
@resolve()
c.publish c.CONTEXT_SYNCED, @, 'success'
@@ -308,6 +323,10 @@ define [
if @id is id or not id and @isSession()
@clear()
+ c.subscribe c.CONTEXT_SAVE, (id) =>
+ if @id is id or not id and @isSession()
+ @save()
+
@resolve()
parse: (resp) =>
@@ -321,11 +340,9 @@ define [
@root.add(node)
return resp
- save: =>
- @set 'json', @root.toJSON()
- c.publish c.CONTEXT_SYNCING, @
+ save: ->
+ @root.save()
super
- return
toJSON: ->
attrs = super
View
2  coffee/cilantro/router.coffee
@@ -70,7 +70,7 @@ define [
_register: (options) ->
if @_registered[options.id]?
- throw new Error "Route #{ id } already registered"
+ throw new Error "Route #{ options.id } already registered"
# Clone since the options options will be augmented
options = _.clone options
View
1  coffee/cilantro/ui/charts/core.coffee
@@ -67,6 +67,7 @@ define [
renderChart: (options) ->
if @chart then @chart.destroy?()
@chart = new Highcharts.Chart(options)
+ @set(@context)
# Set a default option for the class
View
3  coffee/cilantro/ui/charts/dist.coffee
@@ -42,7 +42,7 @@ define [
options.chart.renderTo = @ui.chart[0]
return options
- getId: -> @model.id
+ getField: -> @model.id
getValue: (options) ->
points = @chart.getSelectedPoints()
@@ -68,7 +68,6 @@ define [
options = @getChartOptions(resp)
@renderChart(options)
-
setValue: (value) =>
if not c._.isArray(value) then value = []
points = @chart.series[0].points
View
27 coffee/cilantro/ui/concept/form.coffee
@@ -21,7 +21,10 @@ define [
constructor: ->
super
session = c.data.contexts.getSession()
- @context = session.fetch(concept: @model.id)
+ @context = session.fetch
+ concept: @model.id
+ ,
+ create: 'branch'
events:
'click .concept-actions [data-toggle=add]': 'save'
@@ -35,14 +38,13 @@ define [
update: '.concept-actions [data-toggle=update]'
regions:
- main: '.concept-main'
- chart: '.concept-chart'
fields: '.concept-fields'
onRender: ->
fields = new field.FieldFormCollection
collection: @model.fields
context: @context
+ hideSingleFieldInfo: true
@fields.show(fields)
@setDefaultState()
@@ -51,7 +53,7 @@ define [
# If this is valid field-level context update the state
# of the concept form. Only one of the fields need to be
# valid to update the context
- if @context?.isValid()
+ if @context?.isValid(strict: true)
@setUpdateState()
else
@setNewState()
@@ -68,16 +70,19 @@ define [
# Saves the current state of the context which enables it to be
# synced with the server.
- save: (options) ->
- options = c._.extend(deep: @options.managed, options)
- @context?.save(options)
+ save: ->
+ options = deep: @options.managed
+ if @context?
+ @context.save(options)
+ c.publish c.CONTEXT_SAVE
@setUpdateState()
# Clears the local context of conditions
- clear: (options) ->
- options = c._.extend(deep: @options.managed, options)
- @context?.clear(options)
- @context?.save(options)
+ clear: ->
+ options = deep: @options.managed
+ if @context?
+ @context.clear(options)
+ @context.save(options)
@setNewState()
View
23 coffee/cilantro/ui/controls/base.coffee
@@ -14,10 +14,10 @@ define [
class Control extends c.Marionette.Layout
className: 'control'
- attrNames: ['id', 'operator', 'value', 'nulls']
+ attrNames: ['field', 'operator', 'value', 'nulls']
regions:
- id: '.control-id'
+ field: '.control-field'
operator: '.control-operator'
value: '.control-value'
nulls: '.control-nulls'
@@ -27,25 +27,25 @@ define [
regionOptions: {}
dataAttrs:
- id: 'data-id'
+ field: 'data-field'
operator: 'data-operator'
value: 'data-value'
nulls: 'data-nulls'
dataSelectors:
- id: '[data-id]'
+ field: '[data-field]'
operator: '[data-operator]'
value: '[data-value]'
nulls: '[data-nulls]'
attrGetters:
- id: 'getId'
+ field: 'getField'
operator: 'getOperator'
value: 'getValue'
nulls: 'getNulls'
attrSetters:
- id: 'setId'
+ field: 'setField'
operator: 'setOperator'
value: 'setValue'
nulls: 'setNulls'
@@ -90,6 +90,8 @@ define [
@[key].show new klass(options)
+ @set(@context)
+
_get: (key, options) ->
if not (method = @attrGetters[key]) then return
if (func = @[method])?
@@ -118,7 +120,10 @@ define [
set: (key, value, options) ->
if key? and typeof key is 'object'
- attrs = key
+ if key instanceof c.Backbone.Model
+ attrs = key.toJSON()
+ else
+ attrs = key
options = value
else
(attrs = {})[key] = value
@@ -146,12 +151,12 @@ define [
@trigger 'change', @, @get()
- getId: ->
+ getField: ->
getOperator: ->
getValue: ->
getNulls: ->
- setId: ->
+ setField: ->
setOperator: ->
setValue: ->
setNulls: ->
View
4 coffee/cilantro/ui/field/controls.coffee
@@ -59,12 +59,12 @@ define [
$el.attr(@dataAttrs[attr], value)
return
- getId: -> @model?.id or @_getAttr('id')
+ getField: -> @model?.id or @_getAttr('field')
getOperator: -> @_getAttr('operator', 'string')
getValue: -> @_getAttr('value', @model?.get('simple_type'))
getNulls: -> @_getAttr('nulls', 'boolean')
- setId: (value) -> not @model?.id and @_setAttr('id', value)
+ setField: (value) -> not @model?.id and @_setAttr('field', value)
setOperator: (value) -> @_setAttr('operator', value)
setValue: (value) -> @_setAttr('value', value)
setNulls: (value) -> @_setAttr('nulls', Boolean(value))
View
30 coffee/cilantro/ui/field/form.coffee
@@ -36,7 +36,7 @@ define [
update: '.field-actions [data-toggle=update]'
regions:
- main: '.field-main'
+ info: '.field-info'
stats: '.field-stats'
control: '.field-control'
chart: '.field-chart'
@@ -45,25 +45,28 @@ define [
# easier to extend. This can also be a function that returns
# an object.
regionViews:
- main: item.Field
+ info: item.Field
stats: stats.FieldStats
control: controls.FieldControl
onRender: ->
for key, klass of c._.result @, 'regionViews'
+ if key is 'info' and @options.hideInfo
+ continue
+
view = new klass
model: @model
context: @context
+
@[key].show view
# Only represent for fields that support distributions
if @options.showChart and @model.links.distribution?
- chart = new charts.FieldChart
+ @chart.show new charts.FieldChart
model: @model
context: @context
chart:
height: 200
- @chart.show chart
@setDefaultState()
@@ -88,13 +91,16 @@ define [
# synced with the server.
save: ->
@context?.save()
- if @options.managed then @setUpdateState()
+ if @options.managed
+ @setUpdateState()
# Clears the local context of conditions
clear: ->
- @context?.clear()
- @context?.save()
- if @options.managed then @setNewState()
+ if @context?
+ @context.clear()
+ @context.save()
+ if @options.managed
+ @setNewState()
class FieldFormCollection extends c.Marionette.CollectionView
@@ -108,6 +114,14 @@ define [
context: context.fetch
field: model.id
concept: context.get 'concept'
+ ,
+ create: 'condition'
+
+ # This collection is used by a concept, therefore if only one
+ # field is present, the concept name and description take
+ # precedence
+ if @options.hideSingleFieldInfo and @collection.length < 2
+ options.hideInfo = true
if not @fieldChartIndex? and model.links.distribution?
@fieldChartIndex = index
View
156 spec/ContextModelSpec.js
@@ -131,16 +131,10 @@ define(['cilantro'], function (c) {
model.remove(node);
});
- it('should clear (but not destroy)', function() {
+ it('should clear', function() {
model.add(node);
model.clear();
- expect(model.get('children')[0].attributes).toEqual({});
- });
-
- it('should clear and destroy', function() {
- model.add(node);
- model.clear({destroy: true});
- expect(model.get('children').length).toBe(0);
+ expect(model.get('children')[0].attributes).toEqual({field: 1, concept: 1});
});
it('should fetch child node (by field)', function() {
@@ -206,108 +200,82 @@ define(['cilantro'], function (c) {
describe('Local vs. Public attributes', function() {
- it('toJSON should recurse on public attributes', function() {
- model.add(node);
-
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: []
- });
-
- model.save({deep: false});
-
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: [{
- field: 1,
- concept: 1,
- value: 30,
- operator: 'exact'
- }]
- });
- });
+ describe('toJSON', function() {
- it('should not recurse unless the deep=true option is passed', function() {
- model.add(node);
- node.set('value', 50);
+ it('should recurse on public attributes', function() {
+ model.add(node);
- model.save({deep: false});
-
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: [{
- field: 1,
- concept: 1,
- value: 30,
- operator: 'exact'
- }]
- });
+ expect(model.toJSON()).toEqual({
+ type: 'and',
+ children: []
+ });
- model.save({deep: true});
+ expect(model.save({deep: false})).toBe(true);
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: [{
- field: 1,
- concept: 1,
- value: 50,
- operator: 'exact'
- }]
+ expect(model.toJSON()).toEqual({
+ type: 'and',
+ children: [{
+ field: 1,
+ concept: 1,
+ value: 30,
+ operator: 'exact'
+ }]
+ });
});
- });
- it('toJSON should not include invalid children by default', function() {
- model.add(node);
+ it('should not recurse unless the deep=true option is passed', function() {
+ model.add(node);
+ node.set('value', 50);
+ expect(model.save({deep: true})).toBe(true);
- expect(model.save()).toBe(true);
-
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: [{
- field: 1,
- concept: 1,
- value: 30,
- operator: 'exact'
- }]
+ expect(model.toJSON()).toEqual({
+ type: 'and',
+ children: [{
+ field: 1,
+ concept: 1,
+ value: 50,
+ operator: 'exact'
+ }]
+ });
});
- // Make the node invalid
- node.set('value', null);
+ it('should ignore invalid children but not remove them', function() {
+ model.add(node);
+ node.set('value', null);
+ expect(model.save({ignore: true})).toBe(true);
- // Still considered valid since the branch itself is valid
- expect(model.save({ignore: false})).toBe(true);
-
- // Invalid node not saved
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: [{
- field: 1,
- concept: 1,
- value: 30,
- operator: 'exact'
- }]
+ expect(model.toJSON()).toEqual({
+ type: 'and',
+ children: [{
+ field: 1,
+ concept: 1,
+ value: 30,
+ operator: 'exact'
+ }]
+ });
});
- // Still considered valid since the branch itself is valid
- expect(model.save()).toBe(true);
+ it('should not ignore invalid children', function() {
+ model.add(node);
+ node.set('value', null);
+ expect(model.save({ignore: false})).toBe(true);
- // Invalid node not saved
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: []
+ // Invalid node not saved
+ expect(model.toJSON()).toEqual({
+ type: 'and',
+ children: []
+ });
});
- expect(model.save({strict: true})).toBe(false);
+ it('should stop processing if strict is true', function() {
+ model.add(node);
+ node.set('value', null);
+ expect(model.save({strict: true})).toBe(false);
- // Invalid node not saved
- expect(model.toJSON()).toEqual({
- type: 'and',
- children: [{
- field: 1,
- concept: 1,
- value: 30,
- operator: 'exact'
- }]
+ expect(model.toJSON()).toEqual({
+ type: 'and',
+ children: []
+ });
});
});
View
8 spec/ControlSpec.js
@@ -13,13 +13,13 @@ define(['cilantro.ui', 'text!../mock/fields.json'], function (c, fieldsJSON) {
control.render();
});
- it('should have an id by default', function() {
- expect(control.get()).toEqual({id: 30});
+ it('should have an field by default', function() {
+ expect(control.get()).toEqual({field: 30});
});
- it('should never clear the id attr', function() {
+ it('should never clear the field attr', function() {
control.clear();
- expect(control.get()).toEqual({id: 30});
+ expect(control.get()).toEqual({field: 30});
});
});
View
2  templates/views/concept-form.html
@@ -9,8 +9,6 @@
<p class=concept-description><%= data.description %></p>
<% } %>
-<div class=concept-chart></div>
-<div class=concept-main></div>
<div class=concept-fields></div>
<div class=concept-actions>
View
4 templates/views/field-form.html
@@ -1,6 +1,6 @@
-<div class=field-main></div>
-<div class=field-stats></div>
+<div class=field-info></div>
<div class=field-chart></div>
+<div class=field-stats></div>
<div class=field-control></div>
<div class=field-actions>
<button data-toggle=add class="btn btn-success btn-mini">Add</button>

No commit comments for this range

Something went wrong with that request. Please try again.