Updates ParserMixin, working on simplifying Scaffolding.
authordsc <dsc@wikimedia.org>
Fri, 11 May 2012 18:00:01 +0000 (11:00 -0700)
committerdsc <dsc@wikimedia.org>
Fri, 11 May 2012 18:00:01 +0000 (11:00 -0700)
17 files changed:
lib/base/base-mixin.co
lib/base/base-model.co
lib/chart/chart-option-model.co
lib/chart/chart-option-view.co
lib/chart/chart-type.co
lib/scaffold/scaffold-model.co
lib/scaffold/scaffold-view.co
lib/template/chart-option.jade
lib/template/chart-scaffold.jade
lib/timeseries/csv.co
lib/timeseries/timeseries.co
lib/util/backbone.co
lib/util/op.co
lib/util/parser.co
msc/labs-notes.md
msc/reportcard-supervisor.conf [new file with mode: 0644]
msc/reportcard.nginx [moved from msc/nginx-count.less.ly.conf with 100% similarity]

index 0b081a7..29a8581 100644 (file)
@@ -7,7 +7,6 @@ Backbone = require 'backbone'
 
 BaseBackboneMixin = exports.BaseBackboneMixin =
     
-    
     initialize: ->
         @__apply_bind__()
     
@@ -142,7 +141,76 @@ BaseBackboneMixin = exports.BaseBackboneMixin =
         "#{@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
+
 
 
index 9723f78..5048a1d 100644 (file)
@@ -13,7 +13,6 @@ Backbone = require 'backbone'
  */
 BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
     
-    
     constructor : function BaseModel
         @__class__      = @constructor
         @__superclass__ = @..__super__.constructor
@@ -32,6 +31,8 @@ BaseModel = exports.BaseModel = Backbone.Model.extend mixinBase do # {{{
     get: (key) ->
         _.getNested @attributes, key
     
+    # TODO: nested sets, handling events
+    
     # set: (key, value, opts) ->
     #     if _.isObject(key) and key?
     #         [values, opts] = [key, value]
@@ -189,19 +190,20 @@ BaseModel import do
 # }}}
 
 
+
 /**
  * @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
index cdba717..2a3c37a 100644 (file)
@@ -1,6 +1,9 @@
-_ = 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'
 
 
 /**
@@ -43,16 +46,34 @@ KNOWN_TAGS = exports.KNOWN_TAGS = new TagSet()
 /**
  * @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
@@ -65,6 +86,9 @@ ChartOption = exports.ChartOption = Field.extend do # {{{
             @set 'ignore', true
     
     
+    
+    ### Tag Handling
+    
     # Wrapper to ensure @set('tags') is called, as tags.push()
     # will not trigger the 'changed:tags' event.
     addTag: (tag) ->
@@ -99,11 +123,50 @@ ChartOption = exports.ChartOption = Field.extend do # {{{
         @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()})"
+
 # }}}
 
 
@@ -111,18 +174,45 @@ ChartOption = exports.ChartOption = Field.extend do # {{{
 /**
  * @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 ...}"
+    
+
 # }}}
 
index 86d86e0..5a6f84f 100644 (file)
@@ -1,6 +1,7 @@
-_ = 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'
 
@@ -10,33 +11,55 @@ DEBOUNCE_RENDER = exports.DEBOUNCE_RENDER = 100ms
 /**
  * @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.`
@@ -65,10 +88,34 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
         @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
+    
     
 # }}}
 
@@ -77,44 +124,92 @@ ChartOptionView = exports.ChartOptionView = FieldView.extend do # {{{
 /**
  * @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'
@@ -124,46 +219,51 @@ ChartOptionScaffold = exports.ChartOptionScaffold = Scaffold.extend do # {{{
             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
+
+
 # }}}
 
 
index 637477b..0831a29 100644 (file)
@@ -1,6 +1,11 @@
-_  = 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'
+
 
 
 /**
@@ -24,9 +29,9 @@ class exports.ChartTypeOption
             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
@@ -119,7 +124,7 @@ class exports.ChartType extends EventEmitter
      * When implementing a ChartType, you can add or override parsers
      * merely by subclassing.
      */
-    this:: import ParserMixin::
+    ParserMixin this
     
     getParserFor: (name) ->
         @getParser @get(name).type
index ee302ab..2800782 100644 (file)
@@ -8,70 +8,35 @@ op = require 'kraken/util/op'
 ### 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
     
@@ -84,32 +49,35 @@ Field = exports.Field = BaseModel.extend do # {{{
         @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.
index 5690062..2a44f93 100644 (file)
@@ -13,8 +13,8 @@ FieldView = exports.FieldView = BaseView.extend do # {{{
     type : 'string'
     
     events :
-        'blur .value'   : 'change'
-        'submit .value' : 'change'
+        'blur .value'   : 'onChange'
+        'submit .value' : 'onChange'
     
     
     constructor: function FieldView
@@ -23,15 +23,9 @@ FieldView = exports.FieldView = BaseView.extend do # {{{
     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
@@ -39,26 +33,29 @@ FieldView = exports.FieldView = BaseView.extend do # {{{
         
         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 ...
     
 # }}}
 
@@ -75,8 +72,8 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
     subviewType    : FieldView
     
     
+    
     constructor: function Scaffold
-        @_subviews = []
         BaseView ...
     
     initialize: ->
@@ -86,22 +83,21 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
         
         @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
@@ -111,7 +107,7 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
         @collection.each @addField
         this
     
-    change: (field) ->
+    onChange: (field) ->
         key   = field.get 'name'
         value = field.getValue()
         @trigger "change:#key", this, value, key, field
@@ -120,8 +116,6 @@ Scaffold = exports.Scaffold = BaseView.extend do # {{{
     
     
 
-
-
 # Proxy collection methods
 <[ get at pluck invoke values toJSON toKVPairs toKV toURL ]>
     .forEach (methodname) ->
index 5fc5084..97353f6 100644 (file)
@@ -8,7 +8,7 @@ include browser-helpers
 - 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") &times;
     
     h3.shortname(title="Click to collapse") #{name}
@@ -17,11 +17,11 @@ section.chart-option.field(id=option_id, class="#{category_cls} #{tags_cls}")
     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}
index 7b7c895..1f49b82 100644 (file)
@@ -7,7 +7,6 @@ form.chart-options.scaffold.form-inline
         .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")
 
index ec87318..84ef3a3 100644 (file)
@@ -53,7 +53,7 @@ class CSVData extends TimeSeriesData
      * @private
      * @returns {this}
      */
-    parse: (@rawData) ->
+    parseData: (@rawData) ->
         return this if typeof rawData is not 'string'
         o = @options
         
index f9ff9cb..2eae2a1 100644 (file)
@@ -31,7 +31,7 @@ class TimeSeriesData
         @labels  = @options.labels or []
         @types   = @options.types  or []
         
-        @parse that if data or @options.data
+        @parseData that if data or @options.data
         @rebuildDerived()
     
     
@@ -75,7 +75,7 @@ class TimeSeriesData
      * Subclass and override to perform preprocessing of the data.
      * @private
      */
-    parse : (rawData) ->
+    parseData : (rawData) ->
         this
     
     /**
index f87fda5..e8a0daf 100644 (file)
@@ -41,6 +41,12 @@ _bb_events =
  */
 _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.
      */
index 83477f0..2fb5172 100644 (file)
@@ -1,3 +1,5 @@
+DASH_PATTERN = /-/g
+
 STRIP_PAT = /(^\s*|\s*$)/g
 strip = (s) ->
     if s then s.replace STRIP_PAT, '' else s
@@ -62,12 +64,20 @@ module.exports = op =
     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
index abcb0ea..42c7c80 100644 (file)
@@ -1,5 +1,7 @@
 _  = require 'kraken/util/underscore'
 op = require 'kraken/util/op'
+{ BaseModel, BaseList, BaseView, Mixin,
+}  = require 'kraken/base'
 
 
 /**
@@ -21,6 +23,12 @@ Parsers = exports.Parsers =
     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
     
@@ -34,12 +42,33 @@ Parsers = exports.Parsers =
             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') ->
@@ -50,7 +79,7 @@ class exports.ParserMixin
         # 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
@@ -66,6 +95,33 @@ class exports.ParserMixin
         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 ...
 
 
index 85a0f06..251ebe0 100644 (file)
@@ -17,17 +17,34 @@ These instances are disposable and our keys are secure enough imo, so I vote con
 
 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
@@ -38,27 +55,49 @@ I'd prefer to do it via package manager (https://github.com/joyent/node/wiki/Ins
        # 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
+```
 
diff --git a/msc/reportcard-supervisor.conf b/msc/reportcard-supervisor.conf
new file mode 100644 (file)
index 0000000..991d759
--- /dev/null
@@ -0,0 +1,5 @@
+[program:reportcard]
+command=/srv/kraken-ui/lib/server/server.co
+directory=/srv/kraken-ui
+user=www-data
+environment=NODE_ENV=prod