BaseBackboneMixin = exports.BaseBackboneMixin =
-
initialize: ->
@__apply_bind__()
"#{@getClassName()}()"
-mixinBase = exports.mixinBase = (body) ->
- _.clone(BaseBackboneMixin) import body
+
+
+/**
+ * @class Base mixin class. Extend this to create a new mixin, attaching the
+ * donor methods as you would instance methods. Your constructor must delegate
+ * to its superclass, passing itself as the context.
+ *
+ * Then, to mingle your mixin with another class or object, invoke the mixin
+ * *without* the `new` operator:
+ *
+ * class MyMixin extends Mixin
+ * -> return Mixin.call MyMixin, it
+ * foo: -> "foo!"
+ *
+ * # Mix into an object...
+ * o = MyMixin { bar:1 }
+ *
+ * # Mix into a Coco class...
+ * class Bar
+ * MyMixin this
+ * bar : 1
+ *
+ */
+class exports.Mixin
+
+ /**
+ * Mixes this mixin into If the target is not a class, a new object will be
+ * returned which inherits from the mixin.
+ * @constructor
+ */
+ (target) ->
+ return unless target
+
+ MixinClass = Mixin
+ MixinClass = @constructor if this instanceof Mixin
+ MixinClass = this if this instanceof Function
+
+ if typeof target is 'function'
+ target:: import MixinClass::
+ else
+ target = _.clone(MixinClass::) import target
+
+ return target
+
+
+
+
+# /**
+# * @returns {Function} Function which takes a target object or class,
+# * mixes the MixinClass into it, and then returns it. If the target is
+# * not a class, a new object will be returned which inherits from the mixin.
+# */
+# makeMixer = exports.makeMixer = (MixinClass) ->
+# mixinBody = if typeof MixinClass is 'function' then MixinClass:: else MixinClass
+# mixinMixer = (target) ->
+# if typeof target is 'function'
+# target:: import mixinBody
+# else
+# target = _.clone(mixinBody) import target
+# target
+#
+# mixinBase = exports.mixinBase = makeMixer BaseBackboneMixin
+
+
+/**
+ * Mixes BaseBackboneMixin into another object or prototype.
+ * @returns {Object} The merged prototype object.
+ */
+mixinBase = exports.mixinBase = (...bodies) ->
+ _.extend _.clone(BaseBackboneMixin), ...bodies
+
*/
BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
-
constructor : function BaseModel
@__class__ = @constructor
@__superclass__ = @..__super__.constructor
get: (key) ->
_.getNested @attributes, key
+ # TODO: nested sets, handling events
+
# set: (key, value, opts) ->
# if _.isObject(key) and key?
# [values, opts] = [key, value]
# }}}
+
/**
* @class Base collection, extending Backbone.Collection, used by scaffold and others.
* @extends Backbone.Collection
*/
BaseList = exports.BaseList = Backbone.Collection.extend mixinBase do # {{{
-
constructor : function BaseList
@__class__ = @constructor
@__superclass__ = @..__super__.constructor
@waitingOn = 0
Backbone.Collection ...
- @trigger 'create', this
+ # @trigger 'create', this
+
getIds: ->
@models.map -> it.id or it.get('id') or it.cid
-_ = require 'kraken/util/underscore'
-{ Field, FieldList, FieldView, Scaffold,
-} = require 'kraken/scaffold'
+{ _, op,
+} = require 'kraken/util'
+{ Parsers, ParserMixin, ParsingModel, ParsingView,
+} = require 'kraken/util/parser'
+{ BaseModel, BaseList,
+} = require 'kraken/base'
/**
/**
* @class Field with chart-option-specific handling for validation, parsing, tags, etc.
*/
-ChartOption = exports.ChartOption = Field.extend do # {{{
+ChartOption = exports.ChartOption = ParsingModel.extend do # {{{
IGNORED_TAGS : <[ callback deprecated debugging ]>
+ valueAttribute : 'value'
+
+ defaults: ->
+ name : ''
+ type : 'String'
+ default : null
+ desc : ''
+ include : 'diff'
+ tags : []
+ examples : []
+
constructor: function ChartOption
- Field ...
+ ParsingModel ...
initialize : ->
# console.log "#this.initialize!"
- Field::initialize ...
+
+ # Bind all the `parseXXX()` methods so they can be passed about independent from the class
+ _.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse'))
+
+ ChartOption.__super__.initialize ...
+ @set 'id', @id = _.camelize @get 'name'
+ @set 'value', @get('default'), {+silent} if not @has 'value'
+
# Notify Tag indexer of category when created, to ensure all category-tags
# get indices with colors :P
@set 'ignore', true
+
+ ### Tag Handling
+
# Wrapper to ensure @set('tags') is called, as tags.push()
# will not trigger the 'changed:tags' event.
addTag: (tag) ->
@getTagIndex @getCategory()
+
+ /* * * Value Accessors * * */
+
+ getValue: (def) ->
+ @getParser() @get @valueAttribute, def
+
+ setValue: (v, options) ->
+ def = @get 'default'
+ if not v and def == null
+ val = null
+ else
+ val = @getParser()(v)
+ @set @valueAttribute, val, options
+
+ clearValue: ->
+ @set @valueAttribute, @get 'default'
+
+ isDefault: ->
+ @get(@valueAttribute) is @get 'default'
+
+
+
+ /* * * Serialization * * */
+
+ /**
+ * Override to default `type` to the model attribute of the same name.
+ * @returns {Function} Parser for the given type.
+ */
+ getParser: (type) ->
+ type or= @get('type') or 'String'
+ ChartOption.__super__.getParser.call this, type
+
+ serializeValue: ->
+ @serialize @getValue()
+
toJSON: ->
- o = Field::toJSON ...
- for k, v in o
- o[k] = '' if v!?
- o
+ {id:@id} import do
+ _.clone(@attributes) import { value:@getValue(), def:@get 'default' }
+
+ toKVPairs: ->
+ { "#{@id}":@serializeValue() }
+
+ toString: -> "(#{@id}: #{@serializeValue()})"
+
# }}}
/**
* @class List of ChartOption fields.
*/
-ChartOptionList = exports.ChartOptionList = FieldList.extend do # {{{
+ChartOptionList = exports.ChartOptionList = BaseList.extend do # {{{
model : ChartOption
constructor: function ChartOptionList
- FieldList ...
+ BaseList ...
+
+
+ /**
+ * Collects a map of fields to their values, excluding those set to `null` or their default.
+ *
+ * @param {Object} [opts={}] Options:
+ * @param {Boolean} [opts.keepDefaults=true] If false, exclude pairs that
+ * haven't changed from their default value.
+ * @param {Boolean} [opts.serialize=false] If true, replace each value
+ * with its String version by calling `value.serializeValue()`.
+ * @returns {Object} Map of fields to their values.
+ */
+ values: (opts={}) ->
+ opts = {+keepDefaults, -serialize} import opts
+ _.synthesize do
+ if opts.keepDefaults then @models else @models.filter -> not it.isDefault()
+ -> [ it.get('name'), if opts.serialize then it.serializeValue() else it.getValue() ]
+
+ toJSON: ->
+ @values {+keepDefaults, -serialize}
/**
* Override to omit defaults from URL.
*/
toKVPairs: ->
- _.collapseObject @values {-keepDefaults, +serialize}
+ _.collapseObject @values {-keepDefaults, +serialize}
+
+ toKV: (item_delim='&', kv_delim='=') ->
+ _.toKV @toKVPairs(), item_delim, kv_delim
+ toURL: (item_delim='&', kv_delim='=') ->
+ "?#{@toKV ...}"
+
+
# }}}
-_ = require 'kraken/util/underscore'
-{ Field, FieldList, FieldView, Scaffold,
-} = require 'kraken/scaffold'
+{ _, op,
+} = require 'kraken/util'
+{ BaseView,
+} = require 'kraken/base'
{ ChartOption, ChartOptionList,
} = require 'kraken/chart/chart-option-model'
/**
* @class View for a single configurable option in a chart type.
*/
-ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
- # __bind__ : <[ onClick ]>
+ChartOptionView = exports.ChartOptionView = BaseView.extend do # {{{
tagName : 'section'
className : 'chart-option field'
template : require 'kraken/template/chart-option'
+ type : 'string'
isCollapsed : true
events :
- 'blur .value' : 'change'
- 'click input[type="checkbox"].value' : 'change'
- 'submit .value' : 'change'
+ 'blur .value' : 'onChange'
+ 'click input[type="checkbox"].value' : 'onChange'
+ 'submit .value' : 'onChange'
'click .close' : 'toggleCollapsed'
'click h3' : 'toggleCollapsed'
'click .collapsed' : 'onClick'
+
constructor: function ChartOptionView
- FieldView ...
+ BaseView ...
+
+ initialize: ->
+ ChartOptionView.__super__.initialize ...
+ @type = @model.get 'type' .toLowerCase() or 'string'
+
+
+ /* * * * Rendering * * * */
+ toTemplateLocals: ->
+ json = ChartOptionView.__super__.toTemplateLocals ...
+ json.id or= _.camelize json.name
+ json.value ?= ''
+ json.value = JSON.stringify v if v = json.value and (_.isArray(v) or _.isPlainObject(v))
+ json
+
+ /**
+ * Override to annotate with collapsed state and to kill off ignored options
+ * so they do not contribute their values when looking at form updates.
+ */
render: ->
- FieldView::render ...
- @$el.addClass 'ignore' if @get 'ignore'
+ return @remove() if @model.get 'ignore'
+ ChartOptionView.__super__.render ...
@$el.addClass 'collapsed' if @isCollapsed
this
+
+ /* * * * Option Collapsing * * * */
+
/**
* Sets the state of `isCollapsed` and updates the UI. If the state changed,
* a `'change:collapse`` event will be fired.`
@collapse not @$el.hasClass 'collapsed'
this
+
+
+ /* * * * Events * * * */
+
+ /**
+ * To prevent `toggleCollapsed()` from being called multiple times due to
+ * overlapping listeners, we're only looking for clicks on the collapsed header.
+ */
onClick: (evt) ->
target = $ evt.target
@toggleCollapsed() if @$el.hasClass('collapsed') and not target.hasClass('close')
+ /**
+ * Propagate user input changes to the model, and upward to the parent view.
+ */
+ onChange: ->
+ if @type is 'boolean'
+ val = !! @$('.value').attr('checked')
+ else
+ val = @model.getParser() @$('.value').val()
+
+ current = @model.getValue()
+ return if _.isEqual val, current
+ console.log "#this.onChange( #current -> #val )"
+ @model.setValue val, {+silent}
+ @trigger 'change', @model, this
+ # false
+
# }}}
/**
* @class View for configuring a chart type.
*/
-ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
- __bind__ : <[ collapseAll expandAll ]>
+ChartOptionScaffold = exports.ChartOptionScaffold = BaseView.extend do # {{{
+ __bind__ : <[ addField ]>
tagName : 'form'
className : 'chart-options scaffold'
template : require 'kraken/template/chart-scaffold'
collectionType : ChartOptionList
subviewType : ChartOptionView
- fields : '.fields'
events:
'click .options-filter-button' : 'onFilterOptions'
'click .collapse-all-options-button' : 'collapseAll'
'click .expand-all-options-button' : 'expandAll'
- # GraphView will set this
- ready : false
constructor: function ChartOptionScaffold
- Scaffold ...
+ BaseView ...
initialize : ->
@render = _.debounce @render.bind(this), DEBOUNCE_RENDER
- Scaffold::initialize ...
+ CollectionType = @collectionType
+ @model = (@collection or= new CollectionType)
+ ChartOptionScaffold.__super__.initialize ...
+
+ @collection.on 'add', @addField, this
+ @collection.on 'reset', @onReset, this
+ @on 'render', @onRender, this
- render: ->
- console.log "#this.render(ready=#{@ready}) -> .isotope()"
- Scaffold::render ...
- return this unless @ready
+ /**
+ * Bookkeeping for new ChartOptions, creating it a new subview and subscribing
+ * to its activity, and then rendering it.
+ * @returns {ChartOptionView} The Option's new view.
+ */
+ addField: (field) ->
+ @removeSubview field.view if field.view
+
+ # avoid duplicating event propagation
+ field.off 'change:value', @onChange, this
+
+ # propagate value-change events as key-value change events
+ field.on 'change:value', @onChange, this
+
+ SubviewType = @subviewType
+ @addSubview view = new SubviewType model:field
+ .on 'change', @onChange.bind(this, field)
+ .on 'change:collapse', @render, this
+
+ @render() # WTF: hmm.
+ view
+
+
+ /* * * * UI * * * */
+
+ /**
+ * Collapse all expanded subviews.
+ * @returns {false} Returns false so event-dispatchers don't propagate
+ * the triggering event (usually a click or submit).
+ */
+ collapseAll: ->
+ _.invoke @subviews, 'collapse', true
+ false
+
+ /**
+ * Expand all collapsed subviews.
+ * @returns {false} Returns false so event-dispatchers don't propagate
+ * the triggering event (usually a click or submit).
+ */
+ expandAll: ->
+ _.invoke @subviews, 'collapse', false
+ false
+
+ /**
+ * Reflow Isotope post-`render()`.
+ */
+ onRender: ->
+ # console.log "#this.onRender(ready=#{@ready}) -> .isotope()"
- container = if @fields then @$ @fields else @$el
- container
- .addClass 'isotope'
- .find '.chart-option.field' .addClass 'isotope-item'
- container.isotope do
+ # The DOM doesn't calculate dimensions of elements that are not visible,
+ # which makes it impossible for Isotope to do its job.
+ return unless @$el.is ':visible'
+
+ # Invoke Isotope to re-layout the option elements
+ @$ '.isotope' .isotope do
# itemPositionDataEnabled : true
itemSelector : '.chart-option.field'
layoutMode : 'masonry'
getSortData :
category: ($el) ->
$el.data 'model' .getCategory()
- this
+ /**
+ * @returns {String} Selector representing the selected set of Option filters.
+ */
getOptionsFilter: ->
data = @$ '.options-filter-button.active' .toArray().map -> $ it .data()
sel = data.reduce do
- (sel, d) ->
- sel += that if d.filter
- sel
+ (sel, d) -> sel += if d.filter then that else ''
':not(.ignore)'
sel
- collapseAll: ->
- _.invoke @subviews, 'collapse', true
- # @renderSubviews()
- false
- expandAll: ->
- _.invoke @subviews, 'collapse', false
- # @renderSubviews()
- false
+ /* * * * Events * * * */
/**
- * Add a ChartOption to this scaffold, rerendering the isotope
- * layout after collapse events.
+ * Propagate change events from fields as if they were attribute changes.
+ * Note: `field` is bound to the handler
*/
- addField: (field) ->
- view = Scaffold::addField ...
- # view.on 'change:collapse render', @render, this
- view.on 'change:collapse', @render, this
- view
-
- toKV: ->
- @collection.toKV ...
+ onChange: (field) ->
+ key = field.get 'name'
+ value = field.getValue()
+ @trigger "change:#key", this, value, key, field
+ @trigger "change", this, value, key, field
+ this
+ onReset: ->
+ # The collection has been reset, assume all subviews are
+ # invalid and rebuild them.
+ @removeAllSubviews()
+ @collection.each @addField
+ _.defer @render
onFilterOptions: (evt) ->
evt.preventDefault()
+ # Defer re-rendering until after we yield for the DOM to do its thang
_.defer @render
+
+# Proxy collection methods
+<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]>
+ .forEach (methodname) ->
+ ChartOptionScaffold::[methodname] = -> @collection[methodname].apply @collection, arguments
+
+
# }}}
-_ = require 'kraken/util/underscore'
-{EventEmitter} = require 'events'
-{Parsers, ParserMixin} = require 'kraken/util/parser'
+{ EventEmitter,
+} = require 'events'
+
+{ _, op,
+} = require 'kraken/util'
+{ Parsers, ParserMixin,
+} = require 'kraken/util/parser'
+
/**
v = @spec[k]
@[k] = v if v?
@tags or= []
- @parse = @chartType.getParser @type
+ @parseValue = @chartType.getParser @type
- parse : Parsers.parseString
+ parseValue : Parsers.parseString
isDefault: (v) ->
# @default is v
* When implementing a ChartType, you can add or override parsers
* merely by subclassing.
*/
- this:: import ParserMixin::
+ ParserMixin this
getParserFor: (name) ->
@getParser @get(name).type
### Scaffold Models
Field = exports.Field = BaseModel.extend do # {{{
- idAttribute : 'name'
valueAttribute : 'value'
+ defaults: ->
+ name : ''
+ type : 'String'
+ default : null
+ desc : ''
+ include : 'diff'
+ tags : []
+ examples : []
+
+
constructor: function Field
BaseModel ...
initialize: ->
_.bindAll this, ...(_.functions this .filter -> _.startsWith(it, 'parse'))
+ @set 'id', @id = _.camelize @get 'name'
@set 'value', @get('default'), {+silent} if not @has 'value'
- # console.log "#this.initialize!"
- # @on 'all', (evt) ~> console.log "#this.trigger(#evt)"
+ Field.__super__.initialize ...
+
+
+
- # Model defaults
- defaults: ->
- {
- name : ''
- type : 'String'
- default : null
- desc : ''
- include : 'diff'
- tags : []
- examples : []
- }
-
-
- /* * * Parsers * * */
-
- getParser: (type) ->
- # XXX: handle 'or' by returning an array of parsers?
- type = _ (type or @get 'type').toLowerCase()
- for t of <[ Integer Float Boolean Object Array Function ]>
- if type.startsWith t.toLowerCase()
- return @["parse#t"]
- @parseString
-
- parseString: (v) ->
- if v? then op.toStr v else null
-
- parseInteger: (v) ->
- r = op.toInt v
- unless isNaN r then r else null
-
- parseFloat: (v) ->
- r = op.toFloat v
- unless isNaN r then r else null
-
- parseBoolean: (v) ->
- op.toBool v
-
- parseArray: (v) ->
- if v then op.toObject v else null
-
- parseObject: (v) ->
- if v then op.toObject v else null
-
- parseFunction: (fn) ->
- if fn and _.startswith String(fn), 'function'
- try eval "(#fn)" catch err then null
- else
- null
/* * * Value Accessors * * */
+
getValue: (def) ->
@getParser() @get @valueAttribute, def
@set @valueAttribute, val, options
clearValue: ->
- @set @valueAttribute, @get('default')
+ @set @valueAttribute, @get 'default'
isDefault: ->
- @get(@valueAttribute) is @get('default')
+ @get(@valueAttribute) is @get 'default'
/* * * Serializers * * */
+
serializeValue: ->
@serialize @getValue()
toJSON: ->
{id:@id} import do
- _.clone(@attributes) import { value:@getValue(), def:@get('default') }
+ _.clone(@attributes) import { value:@getValue(), def:@get 'default' }
toKVPairs: ->
{ "#{@id}":@serializeValue() }
toString: -> "(#{@id}: #{@serializeValue()})"
+
# }}}
FieldList = exports.FieldList = BaseList.extend do # {{{
- model : Field
+ model : Field
+ constructor: function FieldList
+ BaseList ...
- constructor: function FieldList then BaseList ...
/**
* Collects a map of fields to their values, excluding those set to `null` or their default.
type : 'string'
events :
- 'blur .value' : 'change'
- 'submit .value' : 'change'
+ 'blur .value' : 'onChange'
+ 'submit .value' : 'onChange'
constructor: function FieldView
initialize: ->
# console.log "#this.initialize!"
BaseView::initialize ...
- @type = @model.get('type').toLowerCase() or 'string'
+ @type = @model.get 'type' .toLowerCase() or 'string'
- toTemplateLocals: ->
- json = {value:v} = @model.toJSON()
- if _.isArray(v) or _.isPlainObject(v)
- json.value = JSON.stringify v
- json
-
- change: ->
+ onChange: ->
if @type is 'boolean'
val = !! @$('.value').attr('checked')
else
current = @model.getValue()
return if _.isEqual val, current
- console.log "#this.change( #current -> #val )"
+ console.log "#this.onChange( #current -> #val )"
@model.setValue val, {+silent}
@trigger 'change', this
- render: ->
- return @remove() if @model.get 'ignore', false
- return BaseView::render ... if @template
-
- name = @model.get 'name'
- id = _.camelize name
- label = name
- value = @model.get 'value'
- value = '' if value!?
-
- @$el.html """
- <label class="name" for="#id">#label</label>
- <input class="value" type="text" id="#id" name="#id" value="#value">
+ toTemplateLocals: ->
+ json = FieldView.__super__.toTemplateLocals ...
+ json.id or= _.camelize json.name
+ json.value ?= ''
+ json.value = JSON.stringify v if v = json.value and (_.isArray(v) or _.isPlainObject(v))
+ json
+
+ /**
+ * A ghetto default template, typically overridden by superclass.
+ */
+ template: (locals) ->
+ $ """
+ <label class="name" for="#{locals.id}">#{locals.name}</label>
+ <input class="value" type="text" id="#{locals.id}" name="#{locals.id}" value="#{locals.value}">
"""
-
- this
+
+ render: ->
+ return @remove() if @model.get 'ignore'
+ FieldView.__super__.render ...
# }}}
subviewType : FieldView
+
constructor: function Scaffold
- @_subviews = []
BaseView ...
initialize: ->
@collection.on 'add', @addField, this
@collection.on 'reset', @resetFields, this
-
- @$el.data { model:@collection, view:this } .addClass @className
+
addField: (field) ->
@removeSubview field.view if field.view
# avoid duplicating event propagation
- field.off 'change:value', @change, this
+ field.off 'change:value', @onChange, this
# propagate value-change events as key-value change events
- field.on 'change:value', @change, this
+ field.on 'change:value', @onChange, this
SubviewType = @subviewType
view = @addSubview new SubviewType model:field
- view.on 'change', @change.bind(this, field)
+ view.on 'change', @onChange.bind(this, field)
@render()
view
@collection.each @addField
this
- change: (field) ->
+ onChange: (field) ->
key = field.get 'name'
value = field.getValue()
@trigger "change:#key", this, value, key, field
-
-
# Proxy collection methods
<[ get at pluck invoke values toJSON toKVPairs toKV toURL ]>
.forEach (methodname) ->
- var tags_cls = tags.map(_.domize('tag')).join(' ')
-section.chart-option.field(id=option_id, class="#{category_cls} #{tags_cls}")
+section.chart-option.field.isotope-item(id=option_id, class="#{category_cls} #{tags_cls}")
a.close(title="Click to collapse") ×
h3.shortname(title="Click to collapse") #{name}
label.name(for=value_id) #{name}
if ( /object|array|function/i.test(type) )
- textarea.value(id=value_id, name=name, class=type_cls) #{value}
+ textarea.value(id=value_id, name=name, class=type_cls, data-bind="value") #{value}
else
- var input_type = (/boolean/i.test(type) ? 'checkbox' : 'text');
- var checked = ((/boolean/i.test(type) && value) ? 'checked' : null);
- input.value(type=input_type, id=value_id, name=name, class=type_cls, value=value, checked=checked)
+ input.value(type=input_type, id=value_id, name=name, class=type_cls, value=value, checked=checked, data-bind="value")
.type(class=type_cls) #{type}
.default(class=type_cls, title="Default: #{def} (#{type})") #{def}
.std-adv-filter-buttons.btn-group.pull-right(data-toggle="buttons-radio")
a.standard-filter-button.options-filter-button.btn.active(href="#", data-filter=".tag_standard") Standard
a.advanced-filter-button.options-filter-button.btn(href="#", data-filter="") Advanced
-
- .fields(data-subview="ChartOptionView")
+ .fields.isotope(data-subview="ChartOptionView")
* @private
* @returns {this}
*/
- parse: (@rawData) ->
+ parseData: (@rawData) ->
return this if typeof rawData is not 'string'
o = @options
@labels = @options.labels or []
@types = @options.types or []
- @parse that if data or @options.data
+ @parseData that if data or @options.data
@rebuildDerived()
* Subclass and override to perform preprocessing of the data.
* @private
*/
- parse : (rawData) ->
+ parseData : (rawData) ->
this
/**
*/
_backbone = do
+ # /**
+ # * Mix the given object or prototype into this class
+ # */
+ # addMixin: (mixin) ->
+ # ...
+
/**
* @returns {Array<Class>} The list of all superclasses for this class or object.
*/
+DASH_PATTERN = /-/g
+
STRIP_PAT = /(^\s*|\s*$)/g
strip = (s) ->
if s then s.replace STRIP_PAT, '' else s
toInt : (v) -> parseInt v
toFloat : (v) -> parseFloat v
toStr : (v) -> String v
- toObject : (v) ->
+ toRegExp : (v) -> new RegExp v
+
+ toObject : (v) ->
if typeof v is 'string' and strip(v)
JSON.parse v
else
v
+ toDate : (v) ->
+ return v if v!? or v instanceof Date
+ return new Date v if typeof v is 'number'
+ return new Date String(v).replace DASH_PATTERN, '/'
+
+
### comparison
cmp : (x,y) -> if x < y then -1 else (if x > y then 1 else 0)
eq : (x,y) -> x == y
_ = require 'kraken/util/underscore'
op = require 'kraken/util/op'
+{ BaseModel, BaseList, BaseView, Mixin,
+} = require 'kraken/base'
/**
parseString: (v) ->
if v? then op.toStr v else null
+ parseDate: (v) ->
+ if v then op.toDate v else null
+
+ parseRegExp: (v) ->
+ if v then op.toRegExp v else null
+
parseArray: (v) ->
if v then op.toObject v else null
null
+# Aliases
+Parsers.parseNumber = Parsers.parseFloat
+
-class exports.ParserMixin
+/**
+ * @class Methods for a class to select parsers by type reflection.
+ * @mixin
+ */
+class exports.ParserMixin extends Mixin
this:: import Parsers
+ (target) ->
+ return Mixin.call ParserMixin, target
+
- parse: (v, type) ->
+ # XXX: So I'm meh about mixing in the Parsers dictionary.
+ #
+ # - Pros: mixing in `parseXXX()` methods makes it easy to
+ # override in the target class.
+ # - Cons: `parse()` is a Backbone method, which bit me once
+ # already, so conflicts aren't unlikely.
+ #
+ # Other ideas:
+ # - Parsers live at `@__parsers__`, and each instance gets its own clone
+ # - Parser lookup uses a Cascade from the object. (Why not just use prototype, tho?)
+
+ parseValue: (v, type) ->
@getParser(type)(v)
getParser: (type='String') ->
# Handle compound/optional types
# XXX: handle 'or' by returning an array of parsers?
type = _ String(type).toLowerCase()
- for t of <[ Integer Float Boolean Object Array Function ]>
+ for t of <[ Integer Float Number Boolean Object Array Function ]>
if type.startsWith t.toLowerCase()
return @["parse#t"]
@defaultParser or @parseString
else
@getParser 'Object'
-
+
+
+
+/**
+ * @class Basic model which mixes in the ParserMixin.
+ * @extends BaseModel
+ * @borrows ParserMixin
+ */
+ParsingModel = exports.ParsingModel = BaseModel.extend ParserMixin do
+ constructor: function ParsingModel then BaseModel ...
+
+
+/**
+ * @class Basic collection which mixes in the ParserMixin.
+ * @extends BaseList
+ * @borrows ParserMixin
+ */
+ParsingList = exports.ParsingList = BaseList.extend ParserMixin do
+ constructor: function ParsingList then BaseList ...
+
+
+/**
+ * @class Basic view which mixes in the ParserMixin.
+ * @extends BaseView
+ * @borrows ParserMixin
+ */
+ParsingView = exports.ParsingView = BaseView.extend ParserMixin do
+ constructor: function ParsingView then BaseView ...
I'd prefer to do it via package manager (https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager) but the latest in the repo is 0.4.x and we need >=0.6.x. So source it is.
+```sh
sudo su -
- aptitude install build-essential libev-dev libssl-dev
+ aptitude install build-essential libev-dev libssl-dev supervisor
mkdir sources; cd sources
# node.js
- wget http://nodejs.org/dist/v0.6.11/node-v0.6.11.tar.gz
- tar -xvf node-v0.6.11.tar.gz
- cd node-v0.6.11
+ wget http://nodejs.org/dist/v0.6.17/node-v0.6.17.tar.gz
+ tar -xvf node-v0.6.17.tar.gz
+ cd node-v0.6.17
./configure && make && make install && make doc
cd ..
-
+```
+
+Alternatively, as I believe a newer version of node is now in the a ppa repo:
+
+```sh
+ sudo su -
+ aptitude install python-software-properties
+ add-apt-repository ppa:chris-lea/node.js
+ aptitude update
+ aptitude install nodejs
+```
+
+Oh, except that the ancient WMF version overrides it. Sigh.
+
+The rest of the instructions are the same.
+
+```sh
# npm
curl http://npmjs.org/install.sh > npm-install.sh
sh ./npm-install.sh
# coco, coffee (global)
npm install -g coco coffee-script
-
+ # give up root
+ exit
+```
## Kraken
- # reminder -- your user's publickey needs to be in my gitosis repo for this to work
- cd /srv
- git clone git@less.ly:kraken-ui.git
- cd kraken-ui
+
+```sh
+ # I don't know why I can't mkdir when this is owned by `www-data:www` @ 775, and I'm in `www`
+ sudo chown $USER:www /srv
+ # reminder -- your user's publickey needs to be in my gitosis repo for this to work.
+ ### you should NOT be root for this part ###
+ cd /srv && git clone git@less.ly:kraken-ui.git && cd kraken-ui
+ mkdir logs
+ npm install
+ coke setup
+
+ # ok, back to root to setup webservers
sudo su -
+ chown -R www-data:www /srv
+ chmod -R g+w /srv
+
+ # supervisord setup
+ cp -f /srv/kraken-ui/msc/reportcard-supervisor.conf /etc/supervisor/conf.d/reportcard.conf
+ supervisorctl update
+ supervisorctl start reportcard
+
+ # ...if using nginx
+ cp -f /srv/kraken-ui/msc/reportcard.nginx /etc/nginx/sites-available/reportcard
+ cd /etc/nginx/sites-enabled
+ ln -sf ../sites-available/reportcard ./
+ nginx -t && { nginx -s reload || nginx }
+
+ # ...if using Apache
cp -f /srv/kraken-ui/msc/reportcard.wmflabs.org.conf /etc/apache2/sites-available/reportcard
cd /etc/apache2/sites-enabled
ln -sf ../sites-available/reportcard ./
cd ../../mods-enabled
ln -sf ../mods-available/proxy* ../mods-available/rewrite* ./
- exit
+ apache2ctl -t && { apache2ctl reload || apache2ctl start }
- mkdir logs
- npm install
- coke setup
- ./lib/server/server.co
- # ./lib/server/server.co >> logs/kraken.log &
+ exit
+```
--- /dev/null
+[program:reportcard]
+command=/srv/kraken-ui/lib/server/server.co
+directory=/srv/kraken-ui
+user=www-data
+environment=NODE_ENV=prod