Revert "fix auth error response and use 401 for malformed header"

This reverts commit 6dc4d10c25db7710b3e4e8e876d425a1e84574c8.

GitOrigin-RevId: 5d25af9026159b0c1e0a53f0d1fd0d9c685b40ee

--- services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
+++ services/web/app/coffee/Features/Authentication/AuthenticationController.coffee
@@ -194,12 +194,10 @@
 			response = new Oauth2Server.Response(res)
 			Oauth2Server.server.authenticate request, response, {}, (err, token) ->
 				if err?
-					# use a 401 status code for malformed header for git-bridge
-					err.code = 401 if err.code == 400 and err.message == 'Invalid request: malformed authorization header'
 					# fall back to v1 on invalid token
 					return AuthenticationController._requireOauthV1Fallback req, res, next if err.code == 401
-					# send all other errors
-					return res.status(err.code).json({error: err.name, error_description: err.message})
+					# bubble up all other errors
+					return next(err)
 				req.oauth =
 					access_token: token.accessToken
 				req.oauth_token = token


Reorganize

Reorganize

--- lib/object.coffee
+++ lib/object.coffee
@@ -41,30 +41,32 @@
         else
             '' + object

+    # Convert Big-endian UCS-2 to Little-endian to support most PDFRreaders
+    swapBytes = (buff) ->
+        l = buff.length
+        if l & 0x01
+            throw new Error("Buffer length must be even")
+        else
+            for i in [0...l - 1] by 2
+                a = buff[i]
+                buff[i] = buff[i+1]
+                buff[i+1] = a
+        return buff
+
     @s: (string) ->
-
-        # Convert Big-endian UCS-2 to Little-endian to support most PDFRreaders
-        swapBytes = (buff) ->
-            l = buff.length
-            if l & 0x01
-                throw new Error("Buffer length must be even")
-            else
-                for i in [0...l - 1] by 2
-                    a = buff[i]
-                    buff[i] = buff[i+1]
-                    buff[i+1] = a
-            buff
-        string = swapBytes(new Buffer('\ufeff' + string, 'ucs-2')).toString('binary')
-
         string = string.replace(/\\/g, '\\\\\\\\')
             .replace(/\(/g, '\\(')
             .replace(/\)/g, '\\)')
             .replace(/</g, '<')
             .replace(/>/g, '>')
             .replace(/&/g, '&')
+
+        string = swapBytes(new Buffer('\ufeff' + string, 'ucs-2')).toString('binary')

-        isString: yes
-        toString: -> string
+        return {
+            isString: yes
+            toString: -> string
+        }

 module.exports = PDFObject
 PDFReference = require './reference'


simplify babylon options

simplify babylon options

--- lib/ripper.coffee
+++ lib/ripper.coffee
@@ -16,31 +16,11 @@
   ]

   parseOptions:
-    loc: true
-    range: true
-    tokens: true
-    tolerant: true
     sourceType: 'module'
+    allowImportExportEverywhere: true
     allowReturnOutsideFunction: true
-    plugins: [
-      'asyncFunctions'
-      'asyncGenerators'
-      'classConstructorCall'
-      'classProperties'
-      'decorators'
-      'doExpressions'
-      'exponentiationOperator'
-      'exportExtensions'
-      'flow'
-      'functionBind'
-      'functionSent'
-      'jsx'
-      'objectRestSpread'
-      'trailingFunctionCommas'
-    ]
-    # Other options which maybe useful
-    # ecmaVersion: Infinity
-    # allowHashBang: true
+    allowSuperOutsideMethod: true
+    plugins: ['*']

   constructor: ->
     @context = new Context


add route for /user-admin (admin only)

add route for /user-admin (admin only)

--- lib/routes.coffee
+++ lib/routes.coffee
@@ -37,6 +37,15 @@
     @render "Home"

 Router.route "/config"
+
+Router.route "/user-admin",
+  template: 'accountsAdmin'
+  onBeforeAction: ->
+    if !Roles.userIsInRole(Meteor.user(), [ 'admin' ])
+      console.log 'redirecting'
+      @redirect '/'
+    else
+      @next()

 Router.route "/driver_lines"


Dispatch update command on workspace

Dispatch update command on workspace

--- lib/package-updates-status-view.coffee
+++ lib/package-updates-status-view.coffee
@@ -1,5 +1,5 @@
 _ = require 'underscore-plus'
-{View} = require 'atom'
+{View} = require 'atom-space-pen-views'

 module.exports =
 class PackageUpdatesStatusView extends View
@@ -13,7 +13,7 @@
     @setTooltip("#{_.pluralize(packages.length, 'package update')} available")
     statusBar.appendRight(this)

-    @subscribe this, 'click', =>
-      @trigger('settings-view:install-packages')
+    @on 'click', =>
+      atom.commands.dispatch(atom.views.getView(atom.workspace), 'settings-view:check-for-package-updates')
       @destroyTooltip()
       @remove()


Re-introduce @async.

Re-introduce @async.

--- lib/worker.coffee
+++ lib/worker.coffee
@@ -135,7 +135,14 @@
   process: ->
     Worker.current = this
     result = @runner.apply(this, @args)
-    @finish result
+    @finish result unless @_async
+
+  async: (fun) ->
+    fun.apply this
+    yield().apply this
+
+  sync: (fun) ->
+    @fiber.run fun

   # We're done!
   finish: (result) ->


controller now requires test_helper

controller now requires test_helper

--- test/javascripts/controllers/paper_manage_test.js.coffee
+++ test/javascripts/controllers/paper_manage_test.js.coffee
@@ -1,3 +1,5 @@
+#= require test_helper
+
 moduleFor 'controller:paperManage', 'PaperManageController',
   setup: ->
     @phase1 = Ember.Object.create position: 1


Made easier to show the repair time

Made easier to show the repair time

--- views/components/main/parts/miniship-parts/panebody.cjsx
+++ views/components/main/parts/miniship-parts/panebody.cjsx
@@ -167,36 +167,37 @@
                       Next. {ship.api_exp[1]}

-

-

-
-                        {ship.api_nowhp} / {ship.api_maxhp}
-
-

-                        <StatusLabel label={@state.label[j]}/>
-

-

-
-                          ★{ship.api_cond}
+                  {
+                    shipStat =
+

+

+
+                            {ship.api_nowhp} / {ship.api_maxhp}
+
+

+                            <StatusLabel label={@state.label[j]}/>
+

+

+
+                              ★{ship.api_cond}
+
+

+

+
+                          <ProgressBar bsStyle={getHpStyle ship.api_nowhp / ship.api_maxhp * 100}
+                                       now={ship.api_nowhp / ship.api_maxhp * 100} />

-

-
-                      {if ship.api_ndock_time
-                        [
-                          <OverlayTrigger key={j} show = {ship.api_ndock_time} placement='top' overlay={<Tooltip id="panebody-repair-time-#{@props.key}-#{j}">{__ 'Repair Time'}: {resolveTime ship.api_ndock_time / 1000}</Tooltip>}>
-                            <ProgressBar key={j} bsStyle={getHpStyle ship.api_nowhp / ship.api_maxhp * 100}
-                                         now={ship.api_nowhp / ship.api_maxhp * 100} />
-                          </OverlayTrigger>
-                        ]
-                      else
-                        [
-                          <ProgressBar key={j} bsStyle={getHpStyle ship.api_nowhp / ship.api_maxhp * 100}
-                                       now={ship.api_nowhp / ship.api_maxhp * 100} />
-                        ]
-                      }
-
-

+                    if ship.api_ndock_time
+                      <OverlayTrigger show = {ship.api_ndock_time} placement='right' overlay={
+                                      <Tooltip id="panebody-repair-time-#{@props.key}-#{j}">
+                                        {__ 'Repair Time'}: {resolveTime ship.api_ndock_time / 1000}
+                                      </Tooltip>}>
+                        {shipStat}
+                      </OverlayTrigger>
+                    else
+                      shipStat
+                  }


warn an ignore on duplicate models in all_models

--- bokehjs/src/coffee/common/load_models.coffee
+++ bokehjs/src/coffee/common/load_models.coffee
@@ -37,13 +37,21 @@

   logger.debug("load_models: start")

+  seen_specs = {}
+
   for model in modelspecs
-    coll = Collections(model['type'])
+    modeltype = model['type']
     attrs = model['attributes']
-    if coll and coll.get(attrs['id'])
+    modelid = attrs['id']
+    coll = Collections(modeltype)
+    if coll and coll.get(modelid)
       oldspecs.push([coll, attrs])
     else
-      newspecs.push([coll, attrs])
+      if modelid of seen_specs
+        logger.warn("load_models: ignoring duplicate model #{ modeltype } (#{ modelid })")
+      else
+        seen_specs[modelid] = true
+        newspecs.push([coll, attrs])

   # add new objects to collections silently
   for coll_attrs in newspecs


I think this comment said the opposite of what it meant. :-)

--- src/client.coffee
+++ src/client.coffee
@@ -200,7 +200,7 @@
   #
   # This should be called when the user explictly signs off from your
   # application, to meet the users' expectation that after they sign off, their
-  # access tokens will be persisted on the machine.
+  # access tokens will not be persisted on the machine.
   #
   # @param {?function(?Dropbox.ApiError)} callback called when
   #   the authentication completes; if successful, the error parameter is


fix scope

fix scope

--- snippets/octicon.cson
+++ snippets/octicon.cson
@@ -1,5 +1,5 @@
 # Generated with Atomizr v0.14.0 - https://github.com/idleberg/atomizr
-'.text.html -.source.gfm':
+'.text.html':
   'octicon-alert':
     'prefix': 'octicon-alert'
     'body': '<${1:span} class=\"octicon octicon-alert$2\">$3</${1:span}>'


fix bug

fix bug

--- routes/api.coffee
+++ routes/api.coffee
@@ -5,6 +5,7 @@
 updata = require '../tools/updata'
 treeModel = require '../models/tree'
 bookModel = require '../models/book'
+render = require '../tools/render'
 router = express.Router()

 #API Route


Fixed extending parsing documents by the styles

--- lib/tasks/build/parse/documents.coffee
+++ lib/tasks/build/parse/documents.coffee
@@ -31,14 +31,15 @@
 	Document.onError onErrorListener = (name) ->
 		parseFile name, "./views/#{name}.html"

+	Document.onParse onParseListener = (file) ->
+		if platform isnt 'node'
+			Document.Style.extendDocumentByStyles file
+		return
+
 	parseFile = (name, path) ->
 		html = fs.readFileSync path, 'utf-8'
 		file = Document.fromHTML name, html
 		Document.parse file
-
-		if platform isnt 'node'
-			Document.Style.extendDocumentByStyles file
-
 		file

 	saveView = (name, view, callback) ->
@@ -63,6 +64,7 @@
 				path: "#{OUT_DIR}/#{name}.json"

 		Document.onError.disconnect onErrorListener
+		Document.onParse.disconnect onParseListener

 		log.end logtime
 		callback null


Teamwork: Separated constructor logic to methods on TeamworkTabView.

--- client/Teamwork/Views/teamworktabview.coffee
+++ client/Teamwork/Views/teamworktabview.coffee
@@ -8,17 +8,26 @@
     @keysRef  = @workspaceRef.child "keys"
     @indexRef = @workspaceRef.child "index"

+    @listenIndexRef()
+    @listenChildAddedOnKeysRef()
+    @listenChildRemovedOnKeysRef()
+    @listenPaneDidShow()
+
+  listenPaneDidShow: ->
     @tabView.on "PaneDidShow", (pane) =>
       @indexRef.set pane.getOptions().indexKey

-    @indexRef.on "value", (snapshot) =>
-      key = snapshot.val()
-      return unless key
-
+  listenChildRemovedOnKeysRef: ->
+    @keysRef.on "child_removed", (snapshot) =>
+      data = snapshot.val()
+      return unless data
+
+      {indexKey} = data
       for pane in @tabView.panes
-        if pane.getOptions().indexKey is key
-          @tabView.showPaneByIndex @tabView.getPaneIndex pane
-
+        if pane.getOptions().indexKey is indexKey
+          @tabView.removePane pane
+
+  listenChildAddedOnKeysRef: ->
     @keysRef.on "child_added", (snapshot) =>
       data    = snapshot.val()
       key     = data.indexKey
@@ -27,14 +36,14 @@

       @createTabFromFirebaseData data  unless isExist

-    @keysRef.on "child_removed", (snapshot) =>
-      data = snapshot.val()
-      return unless data
-
-      {indexKey} = data
+  listenIndexRef: ->
+    @indexRef.on "value", (snapshot) =>
+      key = snapshot.val()
+      return unless key
+
       for pane in @tabView.panes
-        if pane.getOptions().indexKey is indexKey
-          @tabView.removePane pane
+        if pane.getOptions().indexKey is key
+          @tabView.showPaneByIndex @tabView.getPaneIndex pane

   createElements: ->
     @tabHandleHolder = new ApplicationTabHandleHolder delegate: this


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -54,7 +54,7 @@
     sum=0
     for val in data
       sum += val.value
-    textRadiusInc = Math.Round(sum / data.length)
+    textRadiusInc = Math.Round(sum / object.keys(data).length)
     if !sum
       arcs.append("svg:text")
         .attr('fill', "#fff")


Agent pie testing

Agent pie testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -286,13 +286,14 @@
         totalTickets = if totalTickets != 0 then totalTickets else 'No Results'
         totalLabel.append('text')
             .text(totalTickets)
-            .attr('font-size',radiusi + 'px')
+            .attr('font-size','0px')
             .attr('text-anchor', 'middle')
             .attr('alignment-baseline', 'central')
             .attr('filter','url(#dropshadow)')
             .style("opacity", 0)
         totalLabel.select('text').transition()
             .duration(1000)
+            .attr('font-size',radiusi + 'px')
             .style("opacity", 1)

         oldPieData = piedata


Agent pie: start/end angle pie transition work

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -413,6 +413,7 @@
             .attr('marker-end', 'url(#circ)')
             #.style('opacity', 0)
         pointers.exit().remove()
+
         pointers.transition().duration(2000).attr('d', (d) ->
             console.log('my d: ',d)
             if d.cx > d.l


Agent pie: pointer stroke width

Agent pie: pointer stroke width

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -311,7 +311,7 @@
             .attr('class', 'pointer')
             .style('fill', 'none')
             .style('stroke', 'black')
-            .attr('stroke-width', 1)
+            .attr('stroke-width', 2)
             .attr('marker-end', 'url(#circ)')
             .style('opacity', 0)
         pointers.exit().remove()


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -173,7 +173,7 @@
       a = (d.startAngle + d.endAngle) / 2
       #trig functions adjusted to use the angle relative
       #to the "12 o'clock" vector:
-      console.log 'ts: a', a
+      #console.log 'ts: a', a
       d.cx = Math.sin(a) * (radius - 75)
       d.cy = -Math.cos(a) * (radius - 75)

@@ -191,7 +191,7 @@
       d.y = -Math.cos(a) * (radius - 20)
       d.b = d.oy = d.y + 5
       d.t = d.y - bbox.height - 5
-      console.log 'ts: d', d
+      #console.log 'ts: d', d
       ### check whether the default position
          overlaps any other labels
       ###


--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -226,6 +226,7 @@
         #        return arc b
         #)
         path.transition().duration(2000).attrTween('d', (d,i) ->
+            console.log('window.oldPieData['+@instanceDataId+']: ',window.oldPieData[@instanceDataId])
             lastPieData = window.oldPieData[@instanceDataId]
             console.log('lastPieData: ',lastPieData)
             #window.oldPieData[myInstanceDataId][i] = window.oldPieData[myInstanceDataId][i] ? []


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -283,7 +283,7 @@
             .duration(1500)
             #.attrTween("d", pieTween)
             .attrTween("d", (d,i) ->
-                console.log("d",(d) -> d.value)
+                console.log("d",d)
                 console.log("i",i)
                 i = d3.interpolate({startAngle: 0, endAngle: 0}, {startAngle: d.startAngle, endAngle: d.endAngle})
                 return (t) ->


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -99,14 +99,14 @@
         x = c[0]
         y = c[1]
         mx = 1.2
-        my = 1.1
+        my = 1.2
         h = Math.sqrt(x*x + y*y)
         r = 100
         if x <= 0
             r = -r
         #return "translate(" + (x/h * labelRadius) + "," + (y/h * labelRadius) + ")"
         #return "translate(" + r + "," + y + ")"#(y/h * labelRadius) + ")"
-        return "translate(" + ((x/h * labelRadius)+r) + "," + y + ")"#(y/h * labelRadius) + ")"
+        return "translate(" + ((x/h * labelRadius)+r) + "," + y*my + ")"#(y/h * labelRadius) + ")"
         )

         .attr('fill', "#fff")


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -4,7 +4,7 @@

   onData: (data) ->
     $(@node).fadeOut().fadeIn()
-    @buildPieStructure()
+    #@buildPieStructure()
     #@render(data.data)
     @update(data.data)


vm: fix subscription fetcher procedure in `JVM::removeFromSubscription`

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -618,10 +618,15 @@
           else callback()

     if group.slug is "koding"
-      account.fetchSubscription (err, subscription) =>
-        return callback err  if err
-        return @remove callback  unless subscription # so this is a free account
-        kallback subscription
+      options = targetOptions: tags: $in: "vm"
+      account.fetchSubscriptions null, options, (err, subscriptions = []) =>
+        return callback err  if err
+
+        subscription = freeSubscription = null
+        for subscription in subscriptions
+          freeSubscription = subscription  if "nosync" in subscription.tags
+
+        kallback subscription or freeSubscription
     else
       group.fetchSubscription (err, subscription) =>
         return callback err  if err


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color2.coffee
+++ src/color2.coffee
@@ -119,7 +119,7 @@
   Color2:: =
     complement: ->
       hsl = @toHsl()
-      @hue(180)
+      @hue(180)

     copy: ->
       Color2(@r, @g, @b, @a)
@@ -147,11 +147,16 @@
       return Color2(g, g, g)

     hue: (degrees) ->
+      @copy().hue$(degrees)
+
+    hue$: (degrees) ->
       hsl = @toHsl()

       hsl[0] = (hsl[0] + degrees) % 360

-      return Color2(hslToRgb(hsl))
+      [@r, @g, @b, @a] = hslToRgb(hsl)
+
+      return this

     lighten: (amount) ->
       shiftLightness(amount, this)


Client: Activity: Added onShowMoreParticipantButtonClick method. It will be updated.

--- client/activity/lib/components/channelparticipantavatars/index.coffee
+++ client/activity/lib/components/channelparticipantavatars/index.coffee
@@ -39,6 +39,12 @@
     if @state.addNewParticipantMode is yes
     then @setState addNewParticipantMode: no
     else @setState addNewParticipantMode: yes
+
+
+  onShowMoreParticipantButtonClick: ->
+
+    console.log 'show more participants clicked'
+

   renderAvatars: ->


Fix bug on multi days events ending the last day of the month, refs #409

--- client/app/views/popover_screens/main.coffee
+++ client/app/views/popover_screens/main.coffee
@@ -119,7 +119,7 @@

         timepickerEvents =
             'focus': ->
-                 $(@).timepicker 'highlightHour'
+                $(@).timepicker 'highlightHour'
             'timepicker.next': ->
                 $("[tabindex=#{+$(@).attr('tabindex') + 1}]").focus()
             'timepicker.prev': ->
@@ -230,7 +230,13 @@

         month = +month - 1 if month # Months are 0 indexed in moment.js

-        setObj = { hour, minute, date, month, year }
+        # The order here is *VERY* important. When updating a moment date
+        # by iterating over the properties of this object, we must set month
+        # before day. For all day event ending the last day of the month, date
+        # is superior to the number of days of the month, and setting the
+        # monthe before the day will produce a wrong date.
+        # See #409
+        setObj = { year, month, date, hour, minute}

     # Loop over controls elements w/o exiting the popover scope


Update docpad.coffee

Update docpad.coffee

--- docpad.coffee
+++ docpad.coffee
@@ -38,9 +38,9 @@

       # The website's scripts
       scripts: [
-        '//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'
-        'js/plugins.js'
-        'js/main.js'
+        'scripts/classie.js'
+        'scripts/mlpushmenu.js'
+        'scripts/app.js'
       ]

       # The website's author


EditorPane: Defer adding line widget.

EditorPane: Defer adding line widget.

--- client/ide/workspace/panes/editorpane.coffee
+++ client/ide/workspace/panes/editorpane.coffee
@@ -154,9 +154,9 @@
       editor     : @getEditor()
       html       : "
#{username}
"

-    manager.addLineWidget options
-
-    @lineWidgets[username] = options
+    KD.utils.defer =>
+      manager.addLineWidget options
+      @lineWidgets[username] = options

   setParticipantCursor: (row, column, username) ->


position_buttons trigger event

position_buttons trigger event

--- app/cells/folio/console/index/position_buttons/position_buttons.coffee
+++ app/cells/folio/console/index/position_buttons/position_buttons.coffee
@@ -19,6 +19,8 @@
     tr.btn.add(rowChildren(tr.btn)).insertBefore tr.target
   else
     tr.btn.add(rowChildren(tr.btn)).insertAfter tr.target
+
+  tr.btn.closest('.f-c-show-for-index').trigger('folioConsoleSwitchedRows')

 getTr = ($btn) ->
   $btnTr = $btn.closest('.f-c-show-for__row')


#311: disable BBT when Zutilo 1.2.10.1 is present; see https://github.com/willsALMANJ/Zutilo/issues/42

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -86,6 +86,23 @@
     Zotero.debug('[better' + '-' + 'bibtex] ' + str, level)

 Zotero.BetterBibTeX.extensionConflicts = ->
+  AddonManager.getAddonByID('zutilo@www.wesailatdawn.com', (zutilo) ->
+    return unless zutilo
+    return if Services.vc.compare(zutilo.version, '1.2.10.1') > 0
+    Zotero.BetterBibTeX.removeTranslators()
+    Zotero.BetterBibTeX.disabled = '''
+      Better BibTeX has been disabled because it has detected conflicting extension "zutilo" 1.2.10.1 or
+      earlier. If have proposed a fix at
+
+      https://github.com/willsALMANJ/Zutilo/issues/42
+
+      Once that has been implemented, Better BibTeX will start up as usual.  Alternately, you can uninstall Zutilo.
+
+      In the meantime, unfortunately, Better BibTeX and recoll-firefox cannot co-exist.
+    '''
+    Zotero.BetterBibTeX.flash('Better BibTeX has been disabled', Zotero.BetterBibTeX.disabled)
+  )
+
   AddonManager.getAddonByID('{359f0058-a6ca-443e-8dd8-09868141bebc}', (recoll) ->
     return unless recoll
     return if Services.vc.compare(recoll.version, '1.2.3') > 0


@mention as a channel for individual

@mention as a channel for individual

--- client/app/Applications/Chat.kdapplication/AppController.coffee
+++ client/app/Applications/Chat.kdapplication/AppController.coffee
@@ -4,6 +4,8 @@
 Channel instance.
 ###
 TOPICREGEX = /#([\w-]+)/g
+MENTIONREGEX = /@([\w-]+)/g
+
 class Chat12345 extends AppController
   {mq} = bongo
   PUBLIC = 'public'
@@ -26,12 +28,16 @@

   loadView:(mainView)->
     @joinChannel PUBLIC
+    @joinChannel "@#{@username}"

   joinChannel: (name) ->
     return @channels[name] if @channels[name]

     view = @getOptions().view
     channelPaneInstance = view.addChannelTab name
+
+    # When the tab is closed, remove the channel reference
+    # and sign the user off from the channel
     channelPaneInstance.on "KDObjectWillBeDestroyed", =>
       delete @channels[name]
       mq.presenceOff @username, name
@@ -49,30 +55,49 @@
       else if status is "unbind"
         channel.removeOfflineUser presence

+    # When the channel's view receives chat input, parse the body
+    # and broadcast it to corresponding channels.
     channel.view.on "ChatMessageSent", (messageBody) =>
-      @parseMessageForChannels messageBody, channel
+      @parseMessage messageBody, channel
       @broadcastOwnMessage messageBody, channel
+      # Also broadcast to public channel
       if name isnt PUBLIC
         @broadcastOwnMessage messageBody, @channels[PUBLIC], channel

+    # Delegates to the channel to handle received message
     @broadcaster.on channelName, (msg) ->
       channel.messageReceived msg

     @channels[name] = channel

-  parseMessageForChannels: (message, fromChannel) ->
+  ###
+  # Parses the message body for any reference to a channel, then
+  # joins the user to that channel. It will then broadcast the
+  # message body to the newly joined channel.
+  ###
+  parseMessage: (message, fromChannel) ->
     while match = TOPICREGEX.exec message
-      channelName = match[1]
+      channelName = match[0]
       channel = @joinChannel channelName
       @broadcastOwnMessage message, channel, fromChannel

+    while match = MENTIONREGEX.exec message
+      mention = match[1]
+      channel = @joinChannel "@#{mention}"
+      @broadcastOwnMessage message, channel, fromChannel
+
+  ###
+  # Broadcasts the message to channel toChannel. If fromChannel
+  # is provided, will set a property on the chat item so that
+  # channel reference will be rendered from the view.
+  ###
   broadcastOwnMessage: (messageBody, toChannel, fromChannel) ->
     chatItem =
       author: @username
       body: messageBody
       meta: {createdAt: new Date().toISOString()}

-    chatItem.channel = "#"+fromChannel.name if fromChannel
+    chatItem.channel = fromChannel.name if fromChannel

     channelMQName = "client-#{toChannel.name}"
     @broadcaster.emit channelMQName, JSON.stringify(chatItem)


Update Atom settings

Update Atom settings

--- .atom/config.cson
+++ .atom/config.cson
@@ -1,14 +1,28 @@
 "*":
   core:
+    ignoredNames: [
+      ".git"
+      ".hg"
+      ".svn"
+      ".DS_Store"
+      "._*"
+      "Thumbs.db"
+      "node_modules"
+    ]
     themes: [
       "one-dark-ui"
       "base16-tomorrow-dark-theme"
     ]
   editor:
-    fontSize: 13
+    fontSize: 12
     scrollPastEnd: true
     showIndentGuide: true
   "exception-reporting":
     userId: "b518a11f-e724-55cb-9ed1-7b68a2616da1"
+  "find-and-replace":
+    scrollToResultOnLiveSearch: true
   welcome:
     showOnStartup: false
+  whitespace:
+    ignoreWhitespaceOnCurrentLine: false
+    keepMarkdownLineBreakWhitespace: false


atom: add binding for open in vim

atom: add binding for open in vim

--- .atom/keymap.cson
+++ .atom/keymap.cson
@@ -33,6 +33,8 @@

   'g f': 'open-this:here'

+  'cmd-shift-v': 'open-vim:open'
+
 '.fuzzy-finder atom-text-editor[mini]':
   'cmd-|': 'pane:split-right'
   'cmd-\\': 'pane:split-down'


lazy load maps

lazy load maps

--- frontend/coffeescript/app/viz/map_view.coffee
+++ frontend/coffeescript/app/viz/map_view.coffee
@@ -185,6 +185,8 @@
             @controls = L.control.layers( null, layers, { collapsed: false } )
             @controls.addTo( @map )

+            @data_offset = 0
+
             # Listen to events
             @map.on( 'moveend', @refresh_viewport )

@@ -225,6 +227,21 @@
           # invalidateSize() doesn't work.
           @map._onResize()

+        get_next_page: () ->
+
+            @data_offset = @data_offset + 1
+            selfie = @
+            @collection.fetch(
+                reset: false
+                data:
+                    geofield: @selected_header.name
+                    bbox: @bounds.toBBoxString()
+                    offset: @data_offset
+                success: (collection, response, options) ->
+                    if response.meta.pages > selfie.data_offset
+                        selfie.get_next_page()
+            )
+
         refresh_viewport: ( event ) =>
             # If we could not find any geofields or the user has not
             # specified an existing one, don't do anything with the
@@ -232,15 +249,23 @@
             if not @selected_header?
                 return

+            @data_offset = 0
+
             # Everytime we move around, grab new data from the server and
             # refresh the viewport!
-            bounds = event.target.getBounds()
+            @bounds = event.target.getBounds()
+
+            selfie = @

             @collection.fetch(
                 reset: true
                 data:
                     geofield: @selected_header.name
-                    bbox: bounds.toBBoxString() )
+                    bbox: @bounds.toBBoxString()
+                success: (collection, response, options) ->
+                    if response.meta.pages > 0
+                        selfie.get_next_page()
+            )

     return DataMapView
 )


Use double arrow on done so the model can be referenced

--- src/scripts/modules/media/editbar/modals/publish.coffee
+++ src/scripts/modules/media/editbar/modals/publish.coffee
@@ -54,7 +54,7 @@
         dataType: 'json'
         xhrFields:
           withCredentials: true
-      .done () ->
+      .done () =>
         # Close editor
         @model.set('editable', false)


Coffee lint, part 2

Coffee lint, part 2

--- src/scripts/modules/media/tabs/contents/contents.coffee
+++ src/scripts/modules/media/tabs/contents/contents.coffee
@@ -8,10 +8,11 @@

   basicNumbering = (nodes, parentNumber) ->
     for node, index in nodes
-      chapterNumber = if parentNumber?
-        "#{parentNumber}.#{index+1}"
-       else
-        index+1
+      chapterNumber =
+        if parentNumber?
+          "#{parentNumber}.#{index+1}"
+        else
+          index+1
       node.set('chapter', chapterNumber)
       if node.isSection()
         childNodes = node.get('contents')?.models


to be fixed differently with #170

to be fixed differently with #170

--- jsapp/xlform/src/view.surveyApp.coffee
+++ jsapp/xlform/src/view.surveyApp.coffee
@@ -305,6 +305,7 @@
     _render_html: ->
       @$el.html $viewTemplates.$$render('surveyApp', @)

+      ###
       @$settings =
         form_id: @$('.form__settings__field--form_id')
         version: @$('.form__settings__field--version')
@@ -323,6 +324,7 @@
         _inp.val(_style_val)
       else
         @$settings.style.find('select').val(_style_val)
+      ###

       @formEditorEl = @$(".-form-editor")
       @settingsBox = @$(".form__settings-meta__questions")


Add support for author lookup using AJAX

--- src/scripts/modules/media/tabs/metadata/metadata.coffee
+++ src/scripts/modules/media/tabs/metadata/metadata.coffee
@@ -11,7 +11,7 @@

   # Lookup because select2 only allows string values
   # but we need to keep the name and the id for each user
-  allUsers = {}
+  users = {}

   return class MetadataView extends FooterTabView
     template: template
@@ -47,34 +47,33 @@
         type: 'select2'
         select2: () ->
           authors = _.map @getProperty('authors'), (item) ->
-            if not item
-              console.log 'item is null KJSDHFSKDJHF'
             if typeof item is 'string'
-              return allUsers[item]
+              return users[item]
             else
-              idText = {id:item.id, text:item.fullname}
-              allUsers["#{idText.id}"] = idText
-              return idText
+              user = {id: item.id, text: item.fullname or item.id}
+              users["#{user.id}"] = user
+              return user

-          # @$el.find('.authors > input').val(_.map(authors, (item) -> item.id))
           _.extend {}, s2Multi,
             id: (item) -> item.id
             # data: authors
-            initSelection: (el, cb) ->
-              cb(authors)
+            initSelection: (el, cb) -> cb(authors)
             multiple: true
             formatResult: (item, $container, query) -> $('

').append(item.text)
             ajax:
-              url: "#{window.location.origin}/users/search"
+              url: "//#{settings.cnxauthoring.host}:#{settings.cnxauthoring.port}/users/search"
               dataType: 'json'
+              params:
+                xhrFields:
+                  withCredentials: true
               data: (term, page) ->
                 q: term # search term

               results: (data, page) -> # parse the results into the format expected by Select2.
                 results: _.map data.users, (item) ->
-                  idText = {id:item.id, text:item.full_name or item.username}
-                  allUsers["#{idText.id}"] = idText
-                  return idText
+                  user = {id: item.id, text: item.fullname or item.id}
+                  users["#{user.id}"] = user
+                  return user

     initialize: () ->
       super()


better error in the case that the user has no $HOME env var

--- src/kite-server/kite-server.coffee
+++ src/kite-server/kite-server.coffee
@@ -139,7 +139,9 @@
       """

   defaultKiteKey: ->
-    joinPath process.env.HOME, '.kite/kite.key'
+    { HOME } = process.env
+    throw new Error "Couldn't find kite.key"  unless HOME?
+    joinPath HOME, '.kite/kite.key'

   onConnection: (ws) ->
     proto = dnodeProtocol @api


Restored retention menu

Restored retention menu

--- muleview/app/view/ChartsView.coffee
+++ muleview/app/view/ChartsView.coffee
@@ -101,7 +101,6 @@

   createRetentionsMenu: ->
     clickHandler = (me) =>
-      return
       Muleview.event "viewChange", @topKeys, me.retention.get("name")

     items = []


fix(test): function execution order seems different on CI server?

--- internal_packages/message-templates/spec/template-store-spec.coffee
+++ internal_packages/message-templates/spec/template-store-spec.coffee
@@ -115,10 +115,12 @@

     describe "when given a draft id", ->
       it "should create a template from the name and contents of the given draft", ->
+        spyOn(TemplateStore, 'trigger')
+        spyOn(TemplateStore, '_populate')
         runs ->
           TemplateStore._onCreateTemplate({draftLocalId: 'localid-b'})
         waitsFor ->
-          fs.writeFile.callCount > 0
+          TemplateStore.trigger.callCount > 0
         runs ->
           expect(TemplateStore.items().length).toEqual(1)


:lipstick: Removes ugly $ access

:lipstick: Removes ugly $ access

--- lib/minimap-quick-settings-view.coffee
+++ lib/minimap-quick-settings-view.coffee
@@ -1,6 +1,5 @@
-{View} = require 'atom-space-pen-views'
+{$, View} = require 'atom-space-pen-views'
 {CompositeDisposable, Emitter} = require 'event-kit'
-$ = View.__super__.constructor

 Minimap = require './main'


Render full collections at once

Render full collections at once

--- app/assets/javascripts/backbone/views/collection_view.js.coffee
+++ app/assets/javascripts/backbone/views/collection_view.js.coffee
@@ -6,25 +6,39 @@

   initialize: ->
     @itemViews = _([])
-    @collection.on 'reset', @render, this
+    @collection.on 'reset', @_reset, this
+    @_reset()

   delegateEvents: ->
     super
     @itemViews.each (v) -> v.delegateEvents()

   render: ->
-    @$el.html @template this
+    $element = $(@template this)
+    $container = $element.filter(@collectionSelector)
+    $container = $element.find(@collectionSelector) if $container.length is 0
+    @itemViews.each (itemView) =>
+      $container.append itemView.render().el
+
+    @$el.html $element
+    this
+
+  _reset: ->
+    @itemViews.invoke 'remove'
+    @itemViews = _([])
     @collection.each (model) =>
       @addItemView model
-    this
+    @render()

   addItemView: (model) ->
     itemView = new @itemViewClass
       model: model
     itemView.parent = this
-    @$(@collectionSelector).append itemView.render().el
+
+
     itemView.on 'all', @_bubbleEvents, this
     @itemViews.push itemView
+    itemView

   findViewForModel: (modelId) ->
     modelId = parseInt modelId, 10


Add $scope.user to address-form directive

--- app/assets/javascripts/sprangular/directives/addressForm.coffee
+++ app/assets/javascripts/sprangular/directives/addressForm.coffee
@@ -6,7 +6,8 @@
     countries: '='
     disabled: '='
     submitted: '='
-  controller: ($scope) ->
+  controller: ($scope, Account) ->
+    $scope.user = Account.user
     $scope.selectedCountry = null
     $scope.hasErrors = false


crop compatibility with old bootstrap

crop compatibility with old bootstrap

--- app/assets/javascripts/uploadcare/ui/crop/crop-widget.js.coffee
+++ app/assets/javascripts/uploadcare/ui/crop/crop-widget.js.coffee
@@ -158,6 +158,8 @@
       @__img = $('<img/>')
         .css
           margin: '0 auto'
+          width: @__resizedSize[0]
+          height: @__resizedSize[1]
         .on 'error', =>
           @__setState 'error'
           @__deferred.reject LOADING_ERROR
@@ -241,4 +243,5 @@
       done = (api) =>
         @__jCropApi = api
         @__setState 'loaded'
-      @__img.Jcrop jCropOptions, -> done this
+      @__img.Jcrop jCropOptions, ->
+        done(this)


added "create new" to active repo view #47

--- app/assets/javascripts/views/admin/repository_panel_view.coffee
+++ app/assets/javascripts/views/admin/repository_panel_view.coffee
@@ -26,19 +26,19 @@
     @activeRepositoriesView = new ActiveRepositoriesView()

     @listenTo(@availableRepositoriesView, "newItem", @addActiveRepository)
+    @listenTo(@, "render", @afterRender)

+  afterRender: ->

-  render: ->
-
-    @$el.html(@template())
-
-    # subviews
     @availableRepositories.show(@availableRepositoriesView)
     @activeRepositories.show(@activeRepositoriesView)

   addActiveRepository: ->

-    #TODO fetch the collection again from the server
-    @activeRepositoriesView.collection.add({id: Math.random(), repository: "scalableminds/auth-proxy", accessToken: 1234567, adminId: 123})
+    @activeRepositoriesView.collection.create(
+      repository : "scalableminds/auth-proxy"
+      accessToken : 1234567
+    , wait : true
+    )


Addressing leak of event listeners

Addressing leak of event listeners

A probable reason for #131
This fixes two places but the problem still exists

--- lib/Components/CallStackPane.coffee
+++ lib/Components/CallStackPane.coffee
@@ -2,13 +2,13 @@
 {TreeView, TreeViewItem, TreeViewUtils} = require './TreeView'
 hg = require 'mercury'
 fs = require 'fs'
-{EventEmitter} = require 'events'
+{EventEmitter} = require '../eventing'
 {h} = hg
 FocusHook = require('./focus-hook');

 #######################################

-removeBreakListener = null
+listeners = []

 log = (msg) -> #console.log(msg)

@@ -126,7 +126,7 @@

   CallStackPane = () ->
     state = builder.root()
-    removeBreakListener = _debugger.onBreak () ->
+    listeners.push _debugger.onBreak () ->
       log "Debugger.break"
       TreeView.reset(state)
       eventEmitter.emit('frame-selected', null)
@@ -168,7 +168,7 @@
   LocalsPane = () ->
     state = builder2.root()
     refresh = () -> TreeView.populate(state)
-    eventEmitter.on 'frame-selected', (frame) ->
+    listeners.push eventEmitter.subscribe 'frame-selected', (frame) ->
       log "Frame selected"
       builder2.selectedFrame = frame
       refresh()
@@ -279,8 +279,7 @@
   WatchPane = () ->
     state = builder3.root()
     refresh = () -> TreeView.populate(state)
-    _debugger.onBreak () =>
-        refresh()
+    listeners.push _debugger.onBreak () -> refresh()
     return state

   WatchPane.render = (state) ->
@@ -293,4 +292,6 @@
   }

 exports.cleanup = () ->
-  removeBreakListener() if removeBreakListener?
+  for remove in listeners
+    log "removing listener"
+    remove()


simplify

simplify

--- spec/BaconSpec.coffee
+++ spec/BaconSpec.coffee
@@ -1713,20 +1713,18 @@
             value.onValue(->)
             value.take(1)
         [0, 1])
-  describe "case X", ->
+  describe "delayed bounce in case Property ended (bug fix)", ->
     expectStreamEvents(
       ->
         bus = new Bacon.Bus()
-        root = Bacon.later(1).toProperty()
+        root = Bacon.once(0).toProperty()
         root.onValue ->
-
         Bacon.later(1).onValue ->
           root.map(->).onValue ->
             bus.push(1)
             bus.end()
         bus
       [1])
-

 describe "Bacon.combineAsArray", ->
   describe "initial value", ->


Removed expression as a transient quality for equality feature -- confusing

--- javascripts/lib/math/expression_equality.coffee
+++ javascripts/lib/math/expression_equality.coffee
@@ -31,14 +31,9 @@
         else
           false

-      isNonExpressionEqual: (other)->
-        @size() == 1 && ref.refine(@first()).isEqual(other)
-
       isEqual: (other)->
         if other instanceof comps.expression
           @isExpressionEqual(other)
-        else
-          @isNonExpressionEqual(other)
     })

     ref.forType(comps.number, {


Reference package states on packages ivar

--- spec/atom-spec.coffee
+++ spec/atom-spec.coffee
@@ -275,8 +275,8 @@
           atom.activatePackage('package-with-serialize-error',  immediate: true)
           atom.activatePackage('package-with-serialization', immediate: true)
           atom.deactivatePackages()
-          expect(atom.packageStates['package-with-serialize-error']).toBeUndefined()
-          expect(atom.packageStates['package-with-serialization']).toEqual someNumber: 1
+          expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
+          expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
           expect(console.error).toHaveBeenCalled()

         it "removes the package's grammars", ->


Copy fixes

Copy fixes

--- client/introapp/main.coffee
+++ client/introapp/main.coffee
@@ -107,7 +107,7 @@
       slogan     : "for Developers"
       subSlogan  : """

-          You will have an amazing virtual machine that is better than your laptop.  It's connected to the internet 100's of times faster.  You can share it with anyone you wish. Clone git repos.  Test and iterate on your code without breaking your setup.
+          You will have an amazing virtual machine that is better than your laptop.  It's connected to the internet 100s of times faster.  You can share it with anyone you wish. Clone git repos.  Test and iterate on your code without breaking your setup.

           It's free. Koding is your new localhost, in the cloud.


Remove old code.

Remove old code.

--- src/angular-schema-form-remote-validator.coffee
+++ src/angular-schema-form-remote-validator.coffee
@@ -14,16 +14,6 @@
   schemaFormDecoratorsProvider.addMapping 'bootstrapDecorator', 'remote-validator', 'directives/decorators/bootstrap/remote-validator/remote-validator.html'
   schemaFormDecoratorsProvider.createDirective 'remote-validator', 'directives/decorators/bootstrap/remote-validator/remote-validator.html'
   return
-
-# angular.module('schemaForm').factory 'remoteFactory', ($http, $q) ->
-#   factory =
-#     validate: (url, key, value) ->
-#       data = {}
-#       data[key] = value
-#       return $http.post url, data
-#         .then (result) ->
-#           return result.data
-#   return factory

 angular.module('schemaForm').directive 'remoteValidator', ($q, $http, $log) ->

@@ -64,20 +54,6 @@
               return $q.reject(response.data.msg)

-
-
-        # deferred = $q.defer()
-        # remoteFactory.validate(url, key, value)
-        #   .then (response) ->
-        #     ngModel.$setValidity 'remote-validator', response.valid
-        #     if response.valid
-        #       # deferred.resolve()
-        #     else
-        #       # deferred.reject()
-        #     scope.$broadcast('schemaFormValidate')
-        # debugger
-        # return deferred.promise
-
       # add a parser that will process each time the value is
       # parsed into the model when the user updates it.
       # ngModel.$parsers.unshift (value) ->


add exit test

add exit test

--- spec/juno-spec.coffee
+++ spec/juno-spec.coffee
@@ -14,6 +14,7 @@
 describe "managing the Julia process", ->
   client = juno.connection.client
   clientStatus = -> [client.isConnected(), client.isActive(), client.isWorking()]
+
   it "recognises the client's state before boot", ->
     expect(clientStatus()).toEqual [false, false, false]

@@ -44,7 +45,12 @@
       evalsimple("2+2").then (result) ->
         expect(result).toBe(4)

-  it "recognises the client's state after exit"
+  it "exits the process", ->
+    waitsForPromise ->
+      evalsimple('exit()').catch ->
+
+  it "recognises the client's state after exit", ->
+    expect(clientStatus()).toEqual [false, false, false]

 describe "rpc", ->


abbreviated B/I/U=bold/italic/underlined and fixed some default colours

--- client/prefs-colours.coffee
+++ client/prefs-colours.coffee
@@ -64,8 +64,7 @@
   '⍝':fg:'#008888'
   '!':fg:'#ff0000'
   'L':fg:'#000088'
-  'cu':lb:'#ff0000'
-  'mb':bg:'#ff0000'
+  'mb':bg:'#ffff88'
   'sc':bg:'#ff8800'
   'mo':bg:'#eeeeee'

@@ -73,12 +72,12 @@
   join G.map (g) ->
     h = $.extend {}, scheme[g.t], v[g.t] # h: effective style
     g.c.split(',').map((x) -> rp + ' ' + x).join(',') + '{' +
-      (h.fg         && "color:#{h.fg};"             || '') +
-      (h.bg         && "background-color:#{h.bg};"  || '') +
-      (h.bold       && 'font-weight:bold;'          || '') +
-      (h.italic     && 'font-style:italic;'         || '') +
-      (h.underlined && 'text-decoration:underline;' || '') +
-      (h.lb         && "border-left-color:#{h.lb};" || '') +
+      (h.fg && "color:#{h.fg};"             || '') +
+      (h.bg && "background-color:#{h.bg};"  || '') +
+      (h.B  && 'font-weight:bold;'          || '') +
+      (h.I  && 'font-style:italic;'         || '') +
+      (h.U  && 'text-decoration:underline;' || '') +
+      (h.lb && "border-left-color:#{h.lb};" || '') +
       '}'

 prefs.hi updateStyle = (v) -> $('#col-style').text renderCSS v, '.ride-win'; return
@@ -97,9 +96,9 @@
       <select id=col-group>#{join G.map (g, i) -> "<option value=#{i}>#{g.s}"}</select>

<input type=checkbox id=col-fg-cb>Foreground <input type=color id=col-fg list=col-list>

<input type=checkbox id=col-bg-cb>Background <input type=color id=col-bg list=col-list>
-
<input type=checkbox id=col-bold      >B
-         <input type=checkbox id=col-italic    >I
-         <input type=checkbox id=col-underlined>U
+
<input type=checkbox id=col-B>B
+         <input type=checkbox id=col-I>I
+         <input type=checkbox id=col-U>U

          <input type=checkbox id=col-lb-cb>Left border <input type=color id=col-lb>

@@ -117,11 +116,12 @@
       selectGroup H[' ']
     return
   $('#col-group').change -> selectGroup +@value; return
-  $('#col-fg        ').change -> model[sel].fg         = @value;      updateSampleStyle(); return
-  $('#col-bg        ').change -> model[sel].bg         = @value;      updateSampleStyle(); return
-  $('#col-bold      ').click  -> model[sel].bold       = +!!@checked; updateSampleStyle(); return
-  $('#col-italic    ').click  -> model[sel].italic     = +!!@checked; updateSampleStyle(); return
-  $('#col-underlined').click  -> model[sel].underlined = +!!@checked; updateSampleStyle(); return
+  $('#col-fg').change -> model[sel].fg = @value;     updateSampleStyle(); return
+  $('#col-bg').change -> model[sel].bg = @value;     updateSampleStyle(); return
+  $('#col-lb').change -> model[sel].lb = @value;     updateSampleStyle(); return
+  $('#col-B').click   -> model[sel].B = +!!@checked; updateSampleStyle(); return
+  $('#col-I').click   -> model[sel].I = +!!@checked; updateSampleStyle(); return
+  $('#col-U').click   -> model[sel].U = +!!@checked; updateSampleStyle(); return
   ['fg', 'bg', 'lb'].forEach (p) ->
     $("#col-#{p}-cb").click ->
       $("#col-#{p}").toggle @checked
@@ -150,7 +150,7 @@
   '''
   return

-props = qw 'fg bg bold italic underlined lb' # properties in style objects
+props = qw 'fg bg B I U lb' # properties in style objects
 getModelAsObject = -> # keyed by token type, contains only diffs from defaults, suitable for putting in localStorage
   v = {}
   for g, i in G
@@ -169,9 +169,9 @@
     $('#col-fg-cb').prop 'checked', !!h.fg; $('#col-fg').val(h.fg).toggle !!h.fg
     $('#col-bg-cb').prop 'checked', !!h.bg; $('#col-bg').val(h.bg).toggle !!h.bg
     $('#col-lb-cb').prop 'checked', !!h.lb; $('#col-lb').val(h.lb).toggle !!h.lb
-    $('#col-bold'      ).prop 'checked', !!h.bold
-    $('#col-italic'    ).prop 'checked', !!h.italic
-    $('#col-underlined').prop 'checked', !!h.underlined
+    $('#col-B').prop 'checked', !!h.B
+    $('#col-I').prop 'checked', !!h.I
+    $('#col-U').prop 'checked', !!h.U
     $('#col-lb-p').toggle !!G[i].showLB
     sel = i
   return


:memo: Remove FIXME in Editor::transpose

--- src/editor.coffee
+++ src/editor.coffee
@@ -1251,11 +1251,9 @@

   # Public: Transposes the current text selections.
   #
-  # FIXME: I have no idea what this function does.
-  #
-  # This only works if there is more than one selection. Each selection is transferred
-  # to the position of the selection after it. The last selection is transferred to the
-  # position of the first.
+  # The text in each selection is reversed so `abcd` would become `dcba`. The
+  # characters before and after the cursor are swapped when the selection is
+  # empty so `x|y` would become `y|x` where `|` is the cursor location.
   transpose: ->
     @mutateSelectedText (selection) =>
       if selection.isEmpty()


Add support for specifying ?to param when sending test emails

--- emails.coffee
+++ emails.coffee
@@ -186,7 +186,7 @@

           @response.end(html, 'utf8')

-    addSend: (template, to) ->
+    addSend: (template) ->
       check template.name, String
       check template.route,
         path: String
@@ -200,6 +200,8 @@
         path: path
         where: 'server'
         action: ->
+          to = @params.query.to or settings.testEmail
+
           if to?
             data = template.route.data and template.route.data.apply(this, arguments)

@@ -218,7 +220,7 @@

           else
             @response.writeHead 400
-            @response.end("No testEmail provided in settings.")
+            @response.end("No testEmail provided.")

   # Init routine. Precompiles all templates provided and
   # setup routes if provided and if in dev mode.
@@ -231,7 +233,7 @@

         if template.route and process.env.NODE_ENV is 'development'
           Routes.addPreview template
-          Routes.addSend template, settings.testEmail
+          Routes.addSend template

   # Export


live events for delete and create channel

--- app/assets/javascripts/backbone/views/chatarea.js.coffee
+++ app/assets/javascripts/backbone/views/chatarea.js.coffee
@@ -9,7 +9,7 @@

   render: ->
     $('.header .logo').after(@template({channels: @options.channels}))
-    $('.close_channel').click(@deleteChannel);
+    $('.close_channel').live('click',@deleteChannel);
     $('.create_channel').click(@createChannel);
     for channel in @options.channels.models
       view = new Kandan.Views.ChannelPane({channel: channel})
@@ -18,7 +18,6 @@
     @

   createChannel: (event)->
-    console.log("createChannel called", event);
     channelName = prompt("What's the channel name?", "New channel")
     channelName = channelName.replace(/^\s+|\s+$/g, '')
     if channelName


Open add new card dialog if no card on db

--- app/assets/javascripts/billing/add_card_visual.js.coffee
+++ app/assets/javascripts/billing/add_card_visual.js.coffee
@@ -16,23 +16,28 @@
       flagButton = $el.hasClass(clazzName)
       $el[{true: 'removeClass', false: 'addClass'}[flagButton]](clazzName)
       return flagButton
-
-  $('.jg-toggle').click (e) ->
-    $parent = $(@).parent()
+
+  toggleBillingInfo = ($el) ->
+    $parent = $el.parent()
     activeClass = simplyToggleClass($parent, 'open')
-    $(@).find('.jg-disclosure').toggleClass('icon-arrow-down icon-arrow-up active')
-    $el = $parent.find('.jg-toggleable')
+    $parent.find('.jg-disclosure').toggleClass('icon-arrow-down icon-arrow-up active')
+    $tgbl = $parent.find('.jg-toggleable')
     if activeClass is false
-      $el.css
+      $tgbl.css
         display: 'inline-block'
       .animo( { animation: 'fadeInDown', duration: 0.2 })
     else
-      $el.animo( { animation: 'fadeOutUp', duration: 0.2, keep: true}, ->
-        $el.css
+      $tgbl.animo( { animation: 'fadeOutUp', duration: 0.2, keep: true}, ->
+        $tgbl.css
           display: 'none'
       )
-
+
     return
+
+  $('.jg-toggle').click (e) ->
+    toggleBillingInfo $(@)
+
+  toggleBillingInfo $('#jg-add-card') unless account_cards.length

   reactivateTooltip = ($el) ->
     $el.tooltip


providing correct omniture/s_code dependancy

--- app/assets/javascripts/lib/analytics/analytics.js.coffee
+++ app/assets/javascripts/lib/analytics/analytics.js.coffee
@@ -1,4 +1,4 @@
-define ['lib/analytics/analytics_auth', 'lib/analytics/analytics_perf', 's_code'], (AnalyticsAuth, AnalyticsPerf) ->
+define ['lib/analytics/analytics_auth', 'lib/analytics/analytics_perf', 'omniture/s_code'], (AnalyticsAuth, AnalyticsPerf) ->

   class Analytics


Moving comments around

Moving comments around

--- src/entity.coffee
+++ src/entity.coffee
@@ -14,50 +14,70 @@
 #         # custom code here ...
 #

-
-# A superconstructor initializing some basic fields.
-# Game object constructors should call this. For example:
-#
-#     class Enemy extends Kona.Entity
-#       constructor: (opts={}) ->
-#         super(opts)
-#         @isEvil = true
-#
-# Constructor options:
-#
-#   * __group__ - (String) The group this entity belongs to, ex: `'enemies'`
-#   * __solid__ - (Boolean) Whether or not this entity is solid, e.g., can this collide with other entities. Default: true.
-#   * __gravity__ - (Boolean) Whether or not this entity is subject to gravity. Default: true
-#   * __speed__ - (Integer) The speed of this entity when moving.
-#   * __facing__ - (String) The direction this entity is facing. Possible values: `'left'` or `'right'`
-#   * __position__ - (Object)
-#     * x: integer x-coordinate on the canvas
-#     * y: integer y-coordinate on the canvas
-#   * __direction__ - (Object) Values representing the current direction of the entity.
-#     * dx: `-1 = left, 1 = right, 0 = stationary`
-#     * dy: `-1 = up, 1 = down, 0 = stationary`
-#   * __box__ - (Object) Values representing the dimensions of the entity. Collisions
-#     are resolved in terms of a rectangular box model.
-#     * width: An integer width, in pixels
-#     * height: An integer height, in pixels
-#   * __sprite__ - (Type)
-#
 class Kona.Entity

   # Class methods
-  @grav = 8
-
+
+  # Strength of gravity (downward pull applied each tick)
+  @grav = 9
+
+  # Load animations on a group of entities.
+  # This simply passed the given configuration to each entity instance in a group
+  #
+  # Parameters:
+  #
+  #   * __group__ - (String) The group the entity belongs to, ex: `'enemies'`
+  #   * __animations__ - (Object) Animation configuration passed into ent.loadAnimations()
+  #      (See entity#loadAnimations)
+  #
+  # Ex:
+  #
+  #     EvilNinja.loadAnimations 'enemies', {
+  #       'run_right' : { width:50,height:55, sheet: 'img/enemies/turtle/run_right.png', active: true }
+  #       'run_left'  : { width:50,height:55, sheet: 'img/enemies/turtle/run_left.png' }
+  #       'die'       : { width:50,height:55, sheet: 'img/enemies/turtle/die.png', next: -> @entity.destroy() }
+  #     }
+  #
   @loadAnimations = (group, animations) ->
     Kona.Engine.queue =>
       list = Kona.Scenes.currentScene.entities[group]
       if list?
         for ent in list
-          # We can multiple types of entity in the same group, e.g. 'enemies'
+          # We can have multiple types of entity in the same group, e.g. 'enemies'
           # Therefore take precaution to only load anims for the correct instances
           ent.loadAnimations(animations) if ent instanceof @

   # Instance methods
+
+  # A superconstructor initializing some basic fields.
+  # All game object constructors should call this. For example:
+  #
+  #     class Enemy extends Kona.Entity
+  #       constructor: (opts={}) ->
+  #         super(opts)
+  #         @isEvil = true
+  #         # etc...
+  #
+  # Constructor options:
+  #
+  #   * __group__ - (String) The group this entity belongs to, ex: `'enemies'`
+  #   * __solid__ - (Boolean) Whether or not this entity is solid, e.g., can this collide with other entities. Default: true.
+  #   * __gravity__ - (Boolean) Whether or not this entity is subject to gravity. Default: true
+  #   * __speed__ - (Integer) The speed of this entity when moving.
+  #   * __facing__ - (String) The direction this entity is facing. Possible values: `'left'` or `'right'`
+  #   * __position__ - (Object)
+  #     * x: integer x-coordinate on the canvas
+  #     * y: integer y-coordinate on the canvas
+  #   * __direction__ - (Object) Values representing the current direction of the entity.
+  #     * dx: `-1 = left, 1 = right, 0 = stationary`
+  #     * dy: `-1 = up, 1 = down, 0 = stationary`
+  #   * __box__ - (Object) Values representing the dimensions of the entity. Collisions
+  #     are resolved in terms of a rectangular box model.
+  #     * width: An integer width, in pixels
+  #     * height: An integer height, in pixels
+  #   * __sprite__ - (Type)
+  #
   constructor: (opts={}) ->
     @group   = opts.group or fail ("entity must have a group")
     @solid   = if opts.solid?   then opts.solid   else true


Fix broken link to embedded Roff patterns

--- grammars/pic.cson
+++ grammars/pic.cson
@@ -449,7 +449,7 @@
 				1: {name: "punctuation.definition.macro.roff"}
 				2: {name: "entity.function.name.roff"}
 				3: {name: "punctuation.definition.filename.roff"}
-				4: {patterns: [include: "#params"]}
+				4: {patterns: [include: "text.roff#params"]}
 		},{

 			# .PS/.PE: Pic


Fix build directory

Fix build directory

--- gulp/build.coffee
+++ gulp/build.coffee
@@ -10,6 +10,6 @@
     del './build', force: true, next

   gulp.task 'build:src', ->
-    gulp.src(['index.coffee', '+(src)/!(*.spec)*.coffee'])
+    gulp.src(['index.coffee', 'src/!(*.spec)*.coffee'])
       .pipe(coffee({bare: true}))
       .pipe(gulp.dest('build/src'))


Adjust watcher glob expression to cover all changes in Jade template files and fix whitespace

--- gulp/tasks.coffee
+++ gulp/tasks.coffee
@@ -14,15 +14,15 @@
 )

 gulp.task('sass', ['bower'], ->
-    vendorFiles = gulp.src('./bower_components/normalize.css/normalize.css')
+  vendorFiles = gulp.src('./bower_components/normalize.css/normalize.css')

-    applicationFiles = gulp.src('./assets/styles/**/*.sass')
-      .pipe(sass({indentedSyntax: true, sourceComments: 'normal'}))
+  applicationFiles = gulp.src('./assets/styles/**/*.sass')
+    .pipe(sass({indentedSyntax: true, sourceComments: 'normal'}))

-    es.concat(vendorFiles, applicationFiles)
-      .pipe(concat('mcw.css'))
-      .pipe(autoprefixer({browsers: 'last 5 versions', cascade: false}))
-      .pipe(gulp.dest('./build/css'))
+  es.concat(vendorFiles, applicationFiles)
+    .pipe(concat('mcw.css'))
+    .pipe(autoprefixer({browsers: 'last 5 versions', cascade: false}))
+    .pipe(gulp.dest('./build/css'))
 )

 gulp.task('jade', ->
@@ -32,19 +32,19 @@
 )

 gulp.task('clean', ->
-    del(['./build/**'])
+  del(['./build/**'])
 )

 gulp.task('build', ['sass', 'jade'])

 gulp.task('server', ['build'], ->
     gulp.src(['build'])
-      .pipe(webserver({host: '0.0.0.0'}))
+    .pipe(webserver({host: '0.0.0.0'}))
 )

 gulp.task('watch', ['server'], ->
   gulp.watch('./assets/**/*.sass', ['sass'])
-  gulp.watch('./templates/*.jade', ['jade'])
+  gulp.watch('./templates/**/*.jade', ['jade'])
 )

 gulp.task('dist', ['build'], ->


Consistently delete back to last backslash

--- services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
@@ -188,8 +188,22 @@
 									leftRange = _.clone(range)
 									rightRange = _.clone(range)
 									# trim to left of cursor
-									leftRange.start.column -= completions.filterText.length;
-									editor.session.remove(leftRange);
+									lineUpToCursor = editor.getSession().getTextRange(
+										new Range(
+											range.start.row,
+											0,
+											range.start.row,
+											range.start.column,
+										)
+									)
+									# Delete back to last backslash, as appropriate
+									lastBackslashIndex = lineUpToCursor.lastIndexOf('\\')
+									if lastBackslashIndex != -1
+										leftRange.start.column = lastBackslashIndex
+									else
+										leftRange.start.column -= completions.filterText.length
+									editor.session.remove(leftRange)
+									# look at text after cursor
 									lineBeyondCursor = editor.getSession().getTextRange(
 										new Range(
 											rightRange.start.row,


allow pending updates to clear in acceptance tests

--- services/document-updater/test/acceptance/coffee/helpers/DocUpdaterClient.coffee
+++ services/document-updater/test/acceptance/coffee/helpers/DocUpdaterClient.coffee
@@ -33,7 +33,17 @@
 				do (update) ->
 					jobs.push (callback) ->
 						DocUpdaterClient.sendUpdate project_id, doc_id, update, callback
-			async.series jobs, callback
+			async.series jobs, (err) ->
+				DocUpdaterClient.waitForPendingUpdates project_id, doc_id, callback
+
+	waitForPendingUpdates: (project_id, doc_id, callback) ->
+		async.retry {times: 30, interval: 100}, (cb) ->
+			rclient.llen keys.pendingUpdates({doc_id}), (err, length) ->
+				if length > 0
+					cb(new Error("updates still pending"))
+				else
+					cb()
+		, callback

 	getDoc: (project_id, doc_id, callback = (error, res, body) ->) ->
 		request.get "http://localhost:3003/project/#{project_id}/doc/#{doc_id}", (error, res, body) ->


Remove `expiresAt` logic from `acceptInvite`

--- services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
+++ services/web/app/coffee/Features/Collaborators/CollaboratorsInviteHandler.coffee
@@ -70,12 +70,6 @@
 					logger.log {err, projectId, inviteId}, "no matching invite found"
 					return callback(err)

-				now = new Date()
-				if invite.expiresAt < now
-					err = new Errors.NotFoundError("invite expired")
-					logger.log {err, projectId, inviteId, expiresAt: invite.expiresAt}, "invite expired"
-					return callback(err)
-
 				# do the thing
 				existing_users = (project.collaberator_refs or [])
 				existing_users = existing_users.concat(project.readOnly_refs or [])


Inching closer to using proper classes for players.

--- application/chrome/content/wesabe/download/Player.coffee
+++ application/chrome/content/wesabe/download/Player.coffee
@@ -3,511 +3,205 @@
 wesabe.require 'dom.*'
 wesabe.require 'xul.UserAgent'

-wesabe.provide 'download.Player', ->
-
-wesabe.download.Player.register = (params) ->
-  @create params, (klass) ->
-    # make sure we put it where wesabe.require expects it
-    wesabe.provide "fi-scripts.#{params.fid}", klass
-
-wesabe.download.Player.create = (params, callback) ->
-  klass = ->
-    # inherit from Player
-    wesabe.lang.extend this, wesabe.download.Player::, override: false
-
-  callback?(klass)
-
-  params.__module__ = klass.__module__
-
-  # the method that decides based on the state of the job and page what to do next
-  klass::dispatches = []
-  # the elements we need to recognize
-  klass.elements = {}
-  # any dispatch filters
-  klass::filters = []
-  # any download callbacks
-  klass::afterDownloadCallbacks = []
-  # after last goal callbacks
-  klass::afterLastGoalCallbacks = []
-  # any alert callbacks
-  klass::alertReceivedCallbacks = []
-  # any confirm callbacks
-  klass::confirmReceivedCallbacks = []
-  # any open callbacks
-  klass::openReceivedCallbacks = []
-  # the Wesabe Financial Institution ID (e.g. us-001078)
-  klass.fid = klass::fid = params.fid
-  # the name of the Financial Institution (e.g. Wells Fargo)
-  klass.org = klass::org = params.org
-  # ofx info in case this is a hybrid
-  klass.ofx = params.ofx
-
-  klass::canHandleGoal = params.canHandleGoal or (-> true)
-
-  modules = [params]
-
-  if params.includes
-    for include in params.includes
-      try
-        modules.push(wesabe.require(include));
-      catch ex
-        throw new Error("Error while requiring #{include} -- check that the file exists and has the correct 'provide' line")
-
-  # dispatchFrames: false
-  if params.dispatchFrames is false
-    klass::filters.push
-      name: 'frame blocker'
-      test: ->
-        if page.defaultView.frameElement
-          wesabe.info "skipping frame page load: ", page.title
-          return false
-
-  if params.filter
-    klass::filters.push
-      name: 'global'
-      test: params.filter
-
-  # userAgent: "Mozilla/4.0 HappyFunBrowser"
-  if params.userAgent
-    klass::userAgent = params.userAgent
-
-  # userAgentAlias: "Firefox"
-  if params.userAgentAlias
-    klass::userAgent = wesabe.xul.UserAgent.getByNamedAlias(params.userAgentAlias)
-
-  for module in modules
-    if module.dispatch
-      klass::dispatches.push
-        name: module.__module__.name
-        callback: module.dispatch
-
-    if module.elements
-      wesabe.lang.extend klass.elements, module.elements, merge: true
-
-    if module.actions
-      wesabe.lang.extend klass::, module.actions
-
-    if module.extensions
-      wesabe.lang.extend klass::, module.extensions
-
-    if module.afterDownload
-      klass::afterDownloadCallbacks.push module.afterDownload
-
-    if module.afterLastGoal
-      klass::afterLastGoalCallbacks.push module.afterLastGoal
-
-    if module.alertReceived
-      klass::alertReceivedCallbacks.push module.alertReceived
-
-    if module.confirmReceived
-      klass::confirmReceivedCallbacks.push module.confirmReceived
-
-    if module.openReceived
-      klass::openReceivedCallbacks.push module.openReceived
-
-    if module.filter
+wesabe.provide 'download.Player', class Player
+  @register: (params) ->
+    @create params, (klass) ->
+      # make sure we put it where wesabe.require expects it
+      wesabe.provide "fi-scripts.#{params.fid}", klass
+
+  @create: (params, callback) ->
+    class klass extends wesabe.download.Player
+      # the Wesabe Financial Institution ID (e.g. com.Chase)
+      @fid: params.fid
+
+      # the name of the Financial Institution (e.g. Chase)
+      @org: params.org
+
+      # ofx info in case this is a hybrid
+      @ofx: params.ofx
+
+      # the elements we need to recognize
+      @elements: {}
+
+      # pass .fid and .org through to the class
+      @::__defineGetter__ 'fid', -> @constructor.fid
+      @::__defineGetter__ 'org', -> @constructor.org
+
+      canHandleGoal: params.canHandleGoal or (-> true)
+
+    callback?(klass)
+
+    params.__module__ = klass.__module__
+
+    # the method that decides based on the state of the job and page what to do next
+    klass::dispatches = []
+    # any dispatch filters
+    klass::filters = []
+    # any download callbacks
+    klass::afterDownloadCallbacks = []
+    # after last goal callbacks
+    klass::afterLastGoalCallbacks = []
+    # any alert callbacks
+    klass::alertReceivedCallbacks = []
+    # any confirm callbacks
+    klass::confirmReceivedCallbacks = []
+    # any open callbacks
+    klass::openReceivedCallbacks = []
+
+    modules = [params]
+
+    if params.includes
+      for include in params.includes
+        try
+          modules.push(wesabe.require(include));
+        catch ex
+          throw new Error("Error while requiring #{include} -- check that the file exists and has the correct 'provide' line")
+
+    # dispatchFrames: false
+    if params.dispatchFrames is false
       klass::filters.push
-        name: module.__module__.name
-        test: module.filter
-
-  return klass
-
-wesabe.download.Player::start = (answers, browser) ->
-  if @userAgent
-    wesabe.xul.UserAgent.set @userAgent
-  else
-    wesabe.xul.UserAgent.revertToDefault()
-
-  # set up the callbacks for page load and download done
-  wesabe.bind browser, 'DOMContentLoaded', (event) =>
-    @onDocumentLoaded browser, wesabe.dom.page.wrap(event.target)
-
-  wesabe.bind 'downloadSuccess', (event, data, filename) =>
-    @job.update 'account.download.success'
+        name: 'frame blocker'
+        test: ->
+          if page.defaultView.frameElement
+            wesabe.info "skipping frame page load: ", page.title
+            return false
+
+    if params.filter
+      klass::filters.push
+        name: 'global'
+        test: params.filter
+
+    # userAgent: "Mozilla/4.0 HappyFunBrowser"
+    if params.userAgent
+      klass::userAgent = params.userAgent
+
+    # userAgentAlias: "Firefox"
+    if params.userAgentAlias
+      klass::userAgent = wesabe.xul.UserAgent.getByNamedAlias(params.userAgentAlias)
+
+    for module in modules
+      if module.dispatch
+        klass::dispatches.push
+          name: module.__module__.name
+          callback: module.dispatch
+
+      if module.elements
+        wesabe.lang.extend klass.elements, module.elements, merge: true
+
+      if module.actions
+        wesabe.lang.extend klass::, module.actions
+
+      if module.extensions
+        wesabe.lang.extend klass::, module.extensions
+
+      if module.afterDownload
+        klass::afterDownloadCallbacks.push module.afterDownload
+
+      if module.afterLastGoal
+        klass::afterLastGoalCallbacks.push module.afterLastGoal
+
+      if module.alertReceived
+        klass::alertReceivedCallbacks.push module.alertReceived
+
+      if module.confirmReceived
+        klass::confirmReceivedCallbacks.push module.confirmReceived
+
+      if module.openReceived
+        klass::openReceivedCallbacks.push module.openReceived
+
+      if module.filter
+        klass::filters.push
+          name: module.__module__.name
+          test: module.filter
+
+    return klass
+
+  start: (answers, browser) ->
+    if @userAgent
+      wesabe.xul.UserAgent.set @userAgent
+    else
+      wesabe.xul.UserAgent.revertToDefault()
+
+    # set up the callbacks for page load and download done
+    wesabe.bind browser, 'DOMContentLoaded', (event) =>
+      @onDocumentLoaded browser, wesabe.dom.page.wrap(event.target)
+
+    wesabe.bind 'downloadSuccess', (event, data, filename) =>
+      @job.update 'account.download.success'
+      @setErrorTimeout 'global'
+
+      wesabe.tryThrow 'Player#downloadSuccess', (log) =>
+        folder = wesabe.io.dir.profile
+        folder.append 'statements'
+        unless folder.exists()
+          wesabe.io.dir.create(folder)
+
+        statement = folder.clone()
+        statement.append new wesabe.ofx.UUID().toString()
+
+        wesabe.io.file.write statement, data
+
+        @job.recordSuccessfulDownload statement, filename, @job.nextDownloadMetadata
+        delete @job.nextDownloadMetadata
+
+    wesabe.bind 'downloadFail', (event) =>
+      wesabe.warn 'Failed to download a statement! This is bad, but a failed job is worse, so we press on'
+      @job.update 'account.download.failure'
+      @setErrorTimeout 'global'
+      @onDownloadSuccessful browser, wesabe.dom.page.wrap(browser.contentDocument)
+
     @setErrorTimeout 'global'
-
-    wesabe.tryThrow 'Player#downloadSuccess', (log) =>
-      folder = wesabe.io.dir.profile
-      folder.append 'statements'
-      unless folder.exists()
-        wesabe.io.dir.create(folder)
-
-      statement = folder.clone()
-      statement.append new wesabe.ofx.UUID().toString()
-
-      wesabe.io.file.write statement, data
-
-      @job.recordSuccessfulDownload statement, filename, @job.nextDownloadMetadata
-      delete @job.nextDownloadMetadata
-
-  wesabe.bind 'downloadFail', (event) =>
-    wesabe.warn 'Failed to download a statement! This is bad, but a failed job is worse, so we press on'
-    @job.update 'account.download.failure'
-    @setErrorTimeout 'global'
-    @onDownloadSuccessful browser, wesabe.dom.page.wrap(browser.contentDocument)
-
-  @setErrorTimeout 'global'
-  # start the security question timeout when the job is suspended
-  wesabe.bind @job, 'suspend', =>
+    # start the security question timeout when the job is suspended
+    wesabe.bind @job, 'suspend', =>
+      @clearErrorTimeout 'action'
+      @clearErrorTimeout 'global'
+      @setErrorTimeout 'security'
+
+    wesabe.bind @job, 'resume', =>
+      @clearErrorTimeout 'security'
+      @setErrorTimeout 'global'
+
+    @answers = answers
+    @runAction 'main', browser
+
+  nextGoal: ->
+    @job.nextGoal()
+
+  onLastGoalFinished: ->
+    wesabe.info 'Finished all goals, running callbacks'
+    for callback in @afterLastGoalCallbacks
+      @runAction callback, @browser, @page
+
+  finish: ->
     @clearErrorTimeout 'action'
     @clearErrorTimeout 'global'
-    @setErrorTimeout 'security'
-
-  wesabe.bind @job, 'resume', =>
     @clearErrorTimeout 'security'
-    @setErrorTimeout 'global'
-
-  @answers = answers
-  @runAction 'main', browser
-
-wesabe.download.Player::nextGoal = ->
-  @job.nextGoal()
-
-wesabe.download.Player::onLastGoalFinished = ->
-  wesabe.info 'Finished all goals, running callbacks'
-  for callback in @afterLastGoalCallbacks
-    @runAction callback, @browser, @page
-
-wesabe.download.Player::finish = ->
-  @clearErrorTimeout 'action'
-  @clearErrorTimeout 'global'
-  @clearErrorTimeout 'security'
-
-wesabe.download.Player::runAction = (name, browser, page, scope) ->
-  module = @constructor.fid
-
-  fn = if wesabe.isFunction(name) then name else @[name]
-  name = if wesabe.isFunction(name) then (name.name or '(anonymous)') else name
-
-  @job.timer.end 'Navigate'
-
-  unless fn
-    throw new Error "Cannot find action '#{name}'! Typo? Forgot to include a file?"
-
-  retval = wesabe.tryThrow "#{module}##{name}", (log) =>
-    url = page and wesabe.taint(page.defaultView.location.href)
-    title = page && wesabe.taint(page.title)
-
-    @job.timer.start 'Action', =>
-      @setErrorTimeout 'action'
-      @history.push
-        name: name
-        url: url
-        title: title
-
-      wesabe.info 'History is ', (hi.name for hi in @history).join(' -> ')
-
-      wesabe.lang.func.callWithScope fn, this, wesabe.lang.extend(
-        browser: browser
-        page: page
-        e: @constructor.elements
-        answers: @answers
-        options: @job.options
-        log: log
-        tmp: @tmp
-        action: @getActionProxy(browser, page)
-        job: @getJobProxy()
-        skipAccount: @skipAccount
-        reload: => @triggerDispatch(browser, page)
-        download: (args...) => @download(args...)
-      , scope or {})
-
-  @job.timer.start 'Navigate', overlap: false
-
-  return retval
-
-wesabe.download.Player::resume = (answers) ->
-  if wesabe.isArray(answers)
-    for {key, value} in answers
-      @answers[key] = value
-  else if wesabe.isObject(answers)
-    # TODO: 2008-11-24 <brian@wesabe.com> -- this is only here until the new style (Array) is in PFC and SSU Service
-    wesabe.lang.extend @answers, answers
-
-  @onDocumentLoaded @browser, @page
-
-wesabe.download.Player::getActionProxy = (browser, page) ->
-  new ActionProxy this, browser, page
-
-wesabe.download.Player::getJobProxy = ->
-  @job
-
-wesabe.download.Player::download = (url, metadata) ->
-  newStatementFile = =>
-    folder = wesabe.io.dir.profile
-    folder.append 'statements'
-    wesabe.io.dir.create(folder) unless folder.exists()
-
-    statement = folder.clone()
-    statement.append new wesabe.ofx.UUID().toString()
-
-    return statement
-
-
-  # allow pre-registering information about the next download
-  if wesabe.isFunction(metadata)
-    callback = metadata
-    metadata = url
-    url = null
-
-    @job.nextDownloadMetadata = metadata
-    callback()
-
-    return
-  else if metadata is undefined
-    metadata = url
-    url = null
-
-    unless metadata.data
-      throw new Error "Expected metadata #{metadata} to have data to write"
-
-    file = newStatementFile()
-    wesabe.io.file.write file, metadata.data
-    @job.recordSuccessfulDownload file, metadata.suggestedFilename, metadata
-
-    return
-
-  metadata = wesabe.lang.extend url: url, (metadata or {})
-
-  wesabe.tryThrow "Player#download(#{url})", (log) =>
-    wesabe.io.download url, newStatementFile(),
-      success: (file, suggestedFilename) =>
-        @job.recordSuccessfulDownload file, suggestedFilename, metadata
-
-      failure: =>
-        @job.recordFailedDownload metadata
-
-
-#
-# Answers whatever security questions are on the page by
-# using the xpaths given in e.security.
-#
-wesabe.download.Player::answerSecurityQuestions = ->
-
-  questions = page.select e.security.questions
-  qanswers  = page.select e.security.answers
-
-  if questions.length isnt qanswers.length
-    wesabe.error "Found ", questions.length, " security questions, but ",
-      qanswers.length, " security question answers to fill"
-    wesabe.error "questions = ", questions
-    wesabe.error "qanswers = ", qanswers
-    return false
-
-  if questions.length is 0
-    wesabe.error "Failed to find any security questions"
-    return false
-
-  questions = (wesabe.lang.string.trim(page.text(q)) for q in questions)
-
-  wesabe.info "Found security questions: ", questions
-  questions = wesabe.untaint questions
-
-  data = questions: []
-  for question, i in questions
-    answer   = answers[question]
-    element  = qanswers[i]
-
-    if answer
-      page.fill element, answer
-    else
-      log.debug "element = ", element, " -- element.type = ", element.type
-      data.questions.push
-        key: question
-        label: question
-        persistent: true
-        type: wesabe.untaint(element.type) or "text"
-
-  if data.questions.length
-    job.suspend 'suspended.missing-answer.auth.security', data
-    return false
-
-  job.update 'auth.security'
-
-  # choose to bypass the security questions if we can
-  page.check e.security.setCookieCheckbox if e.security.setCookieCheckbox
-  page.fill e.security.setCookieSelect, e.security.setCookieOption if e.security.setCookieSelect
-  # submit the form
-  page.click e.security.continueButton
-
-  return true
-
-#
-# Fills in the date range for a download based on a lower bound.
-#
-# ==== Options (options)
-# :since<Number, null>::
-#   Time of the lower bound to use for the date range (in ms since epoch).
-#
-# @public
-#
-wesabe.download.Player::fillDateRange = ->
-  formatString = e.download.date.format or 'MM/dd/yyyy'
-
-  opts   = e.download.date
-  fromEl = wesabe.untaint page.find(opts.from)
-  toEl   = wesabe.untaint page.find(opts.to)
-
-  getDefault = (defaultValue, existing) =>
-    if wesabe.isFunction(defaultValue)
-      defaultValue = defaultValue(existing)
-
-    wesabe.lang.date.parse(defaultValue) if defaultValue
-
-  if toEl
-    to = wesabe.dom.date.forElement(toEl, formatString)
-    # use default or today's date if we can't get a date from the field
-    to.date ||= getDefault(opts.defaults && opts.defaults.to) or new Date()
-
-    log.info "Adjusting date upper bound: ", to.date
-
-  if fromEl
-    # if there's a lower bound, choose a week before it to ensure some overlap
-    since = options.since and (options.since - 7 * wesabe.lang.date.DAYS)
-
-    # get a date if there's already one in the field
-    from = wesabe.dom.date.forElement fromEl, formatString
-
-    if from.date and since
-      # choose the most recent of the pre-populated date and the lower bound
-      from.date = new Date(Math.max(since, from.date.getTime()))
-    else if since
-      # choose the lower bound
-      from.date = new Date(since)
-    else if to
-      # pick the default or an 89 day window
-      from.date = getDefault(opts.defaults and opts.defaults.from, to: to.date) or
-        wesabe.lang.date.add(to.date, -89 * wesabe.lang.date.DAYS)
-
-    log.info "Adjusting date lower bound: ", from.date
-
-
-wesabe.download.Player::nextAccount = ->
-  delete tmp.account
-  reload()
-
-
-wesabe.download.Player::skipAccount = (args...) ->
-  wesabe.warn(args...) if args.length
-  delete @tmp.account
-
-wesabe.download.Player::actionTimeoutDuration = 60000 # 1m
-wesabe.download.Player::globalTimeoutDuration = 300000 # 5m
-wesabe.download.Player::securityTimeoutDuration = 180000 # 3m
-
-wesabe.download.Player::setErrorTimeout = (type) ->
-  duration = @["#{type}TimeoutDuration"]
-  tt = @_timeouts
-  tt ||= @_timeouts = {}
-
-  @clearErrorTimeout type
-
-  wesabe.debug "Timeout ", type, " set (",duration,")"
-
-  tt[type] = setTimeout =>
-    wesabe.trigger this, 'timeout', [type]
-    return if @job.done
-    wesabe.error "Timeout ",type," (",duration,") reached, abandoning job"
-    wesabe.tryCatch "Player#setErrorTimeout(page dump)", =>
-      @page?.dumpPrivately()
-    @job.fail 504, "timeout.#{type}"
-  , duration
-
-wesabe.download.Player::clearErrorTimeout = (type) ->
-  if @_timeouts?[type]
-    wesabe.debug "Timeout ", type, " cleared"
-    clearTimeout @_timeouts[type]
-
-wesabe.download.Player::onDocumentLoaded = (browser, page) ->
-  return if @job.done or @job.paused
-
-  module = @constructor.fid
-
-  # log when alert and confirm are called
-  new wesabe.dom.Bridge page.proxyTarget, ->
-    @evaluate ->
-      # evaluated on the page
-      window.alert = (message) ->
-        callback 'alert', message
-        return true
-      window.confirm = (message) ->
-        callback 'confirm', message
-        return true
-      window.open = (url) ->
-        callback 'open', url
-        return false
-
-    , (data) ->
-      # evaluated here
-        unless data
-          wesabe.debug "Bridge connected"
-          return
-
-        [type, message] = data
-
-        switch type
-          when 'alert'
-            wesabe.info type, ' called with message=', wesabe.util.inspectForLog(message)
-
-          when 'confirm'
-            wesabe.info type, ' called with message=', wesabe.util.inspectForLog(message), ', automatically answered YES'
-
-          when 'open'
-            wesabe.info type, ' called with url=', wesabe.util.inspectForLog(message)
-
-        callbacks = @["#{type}ReceivedCallbacks"]
-        if callbacks
-          for callback in callbacks
-            wesabe.lang.func.callWithScope callback, this,
-              message: message
-              browser: browser
-              page: page
-              e: @constructor.elements
-              answers: @answers
-              options: @job.options
-              log: wesabe
-              tmp: @tmp
-              action: @getActionProxy browser, page
-              job: @getJobProxy()
-              reload: => @triggerDispatch browser, page
-              skipAccount: @skipAccount
-              download: (args...) => @download args...
-            , [message]
-
-  unless @shouldDispatch browser, page
-    wesabe.info 'skipping document load'
-    return
-
-  @triggerDispatch browser, page
-
-wesabe.download.Player::triggerDispatch = (browser, page) ->
-  module = @constructor.fid
-
-  browser = browser or @browser
-  page = page or @page
-
-  url = wesabe.taint(page.defaultView.location.href)
-  title = wesabe.taint(page.title)
-
-  wesabe.info 'url=', url
-  wesabe.info 'title=', title
-
-  wesabe.trigger this, 'page-load', [browser, page]
-
-  # these should not be used inside the FI scripts
-  @browser = browser
-  @page = page
-
-  @job.timer.start 'Sleep', overlap: false
-
-  setTimeout =>
-    return if @job.done or @job.paused
-
-    for dispatch in @dispatches
-      result = wesabe.tryThrow "#{module}#dispatch(#{dispatch.name})", (log) =>
-        @job.timer.start 'Dispatch', overlap: false
-
-        wesabe.lang.func.callWithScope dispatch.callback, this,
+
+  runAction: (name, browser, page, scope) ->
+    module = @constructor.fid
+
+    fn = if wesabe.isFunction(name) then name else @[name]
+    name = if wesabe.isFunction(name) then (name.name or '(anonymous)') else name
+
+    @job.timer.end 'Navigate'
+
+    unless fn
+      throw new Error "Cannot find action '#{name}'! Typo? Forgot to include a file?"
+
+    retval = wesabe.tryThrow "#{module}##{name}", (log) =>
+      url = page and wesabe.taint(page.defaultView.location.href)
+      title = page && wesabe.taint(page.title)
+
+      @job.timer.start 'Action', =>
+        @setErrorTimeout 'action'
+        @history.push
+          name: name
+          url: url
+          title: title
+
+        wesabe.info 'History is ', (hi.name for hi in @history).join(' -> ')
+
+        wesabe.lang.func.callWithScope fn, this, wesabe.lang.extend(
           browser: browser
           page: page
           e: @constructor.elements
@@ -515,62 +209,371 @@
           options: @job.options
           log: log
           tmp: @tmp
-          action: @getActionProxy browser, page
+          action: @getActionProxy(browser, page)
           job: @getJobProxy()
-          reload: => @triggerDispatch browser, page
           skipAccount: @skipAccount
-          download: (args...) => @download args...
-
-      if result is false
-        wesabe.info "dispatch chain halted"
-        return
-  , 2000
-
-wesabe.download.Player::onDownloadSuccessful = (browser, page) ->
-  for callback in @afterDownloadCallbacks
-    @runAction callback, browser, page
-
-wesabe.download.Player::shouldDispatch = (browser, page) ->
-  for filter in @filters
-    result = wesabe.tryCatch "#{@constructor.fid}#filter(#{filter.name})", (log) =>
-      r = wesabe.lang.func.callWithScope filter.test, this,
-        browser: browser
-        page: page
-        e: @constructor.elements
-        log: log
-        tmp: @tmp
-        job: @getJobProxy()
-        skipAccount: @skipAccount
-
-      if r is true
-        log.debug "forcing dispatch"
-      else if r is false
-        log.debug "aborting dispatch"
-
-      return r
-
-    # check for a definite answer
-    return result if wesabe.isBoolean result
-
-  wesabe.debug "no filter voted to force or abort dispatch, so forcing dispatch by default"
-  return true
-
-wesabe.download.Player::__defineGetter__ 'history', ->
-  @_history ||= []
-
-wesabe.download.Player::__defineGetter__ 'tmp', ->
-  @_tmp ||= {}
-
-wesabe.download.Player.build = (fid) ->
-  wesabe.tryThrow "download.Player.build(fid=#{fid})", (log) =>
-    klass = wesabe.tryThrow "loading fi-scripts.#{fid}", =>
-      wesabe.require 'fi-scripts.' + fid
-
-    new klass(fid)
+          reload: => @triggerDispatch(browser, page)
+          download: (args...) => @download(args...)
+        , scope or {})
+
+    @job.timer.start 'Navigate', overlap: false
+
+    return retval
+
+  resume: (answers) ->
+    if wesabe.isArray(answers)
+      for {key, value} in answers
+        @answers[key] = value
+    else if wesabe.isObject(answers)
+      # TODO: 2008-11-24 <brian@wesabe.com> -- this is only here until the new style (Array) is in PFC and SSU Service
+      wesabe.lang.extend @answers, answers
+
+    @onDocumentLoaded @browser, @page
+
+  getActionProxy: (browser, page) ->
+    new ActionProxy this, browser, page
+
+  getJobProxy: ->
+    @job
+
+  download: (url, metadata) ->
+    newStatementFile = =>
+      folder = wesabe.io.dir.profile
+      folder.append 'statements'
+      wesabe.io.dir.create(folder) unless folder.exists()
+
+      statement = folder.clone()
+      statement.append new wesabe.ofx.UUID().toString()
+
+      return statement
+
+
+    # allow pre-registering information about the next download
+    if wesabe.isFunction(metadata)
+      callback = metadata
+      metadata = url
+      url = null
+
+      @job.nextDownloadMetadata = metadata
+      callback()
+
+      return
+    else if metadata is undefined
+      metadata = url
+      url = null
+
+      unless metadata.data
+        throw new Error "Expected metadata #{metadata} to have data to write"
+
+      file = newStatementFile()
+      wesabe.io.file.write file, metadata.data
+      @job.recordSuccessfulDownload file, metadata.suggestedFilename, metadata
+
+      return
+
+    metadata = wesabe.lang.extend url: url, (metadata or {})
+
+    wesabe.tryThrow "Player#download(#{url})", (log) =>
+      wesabe.io.download url, newStatementFile(),
+        success: (file, suggestedFilename) =>
+          @job.recordSuccessfulDownload file, suggestedFilename, metadata
+
+        failure: =>
+          @job.recordFailedDownload metadata
+
+
+  #
+  # Answers whatever security questions are on the page by
+  # using the xpaths given in e.security.
+  #
+  answerSecurityQuestions: ->
+
+    questions = page.select e.security.questions
+    qanswers  = page.select e.security.answers
+
+    if questions.length isnt qanswers.length
+      wesabe.error "Found ", questions.length, " security questions, but ",
+        qanswers.length, " security question answers to fill"
+      wesabe.error "questions = ", questions
+      wesabe.error "qanswers = ", qanswers
+      return false
+
+    if questions.length is 0
+      wesabe.error "Failed to find any security questions"
+      return false
+
+    questions = (wesabe.lang.string.trim(page.text(q)) for q in questions)
+
+    wesabe.info "Found security questions: ", questions
+    questions = wesabe.untaint questions
+
+    data = questions: []
+    for question, i in questions
+      answer   = answers[question]
+      element  = qanswers[i]
+
+      if answer
+        page.fill element, answer
+      else
+        log.debug "element = ", element, " -- element.type = ", element.type
+        data.questions.push
+          key: question
+          label: question
+          persistent: true
+          type: wesabe.untaint(element.type) or "text"
+
+    if data.questions.length
+      job.suspend 'suspended.missing-answer.auth.security', data
+      return false
+
+    job.update 'auth.security'
+
+    # choose to bypass the security questions if we can
+    page.check e.security.setCookieCheckbox if e.security.setCookieCheckbox
+    page.fill e.security.setCookieSelect, e.security.setCookieOption if e.security.setCookieSelect
+    # submit the form
+    page.click e.security.continueButton
+
+    return true
+
+  #
+  # Fills in the date range for a download based on a lower bound.
+  #
+  # ==== Options (options)
+  # :since<Number, null>::
+  #   Time of the lower bound to use for the date range (in ms since epoch).
+  #
+  # @public
+  #
+  fillDateRange: ->
+    formatString = e.download.date.format or 'MM/dd/yyyy'
+
+    opts   = e.download.date
+    fromEl = wesabe.untaint page.find(opts.from)
+    toEl   = wesabe.untaint page.find(opts.to)
+
+    getDefault = (defaultValue, existing) =>
+      if wesabe.isFunction(defaultValue)
+        defaultValue = defaultValue(existing)
+
+      wesabe.lang.date.parse(defaultValue) if defaultValue
+
+    if toEl
+      to = wesabe.dom.date.forElement(toEl, formatString)
+      # use default or today's date if we can't get a date from the field
+      to.date ||= getDefault(opts.defaults && opts.defaults.to) or new Date()
+
+      log.info "Adjusting date upper bound: ", to.date
+
+    if fromEl
+      # if there's a lower bound, choose a week before it to ensure some overlap
+      since = options.since and (options.since - 7 * wesabe.lang.date.DAYS)
+
+      # get a date if there's already one in the field
+      from = wesabe.dom.date.forElement fromEl, formatString
+
+      if from.date and since
+        # choose the most recent of the pre-populated date and the lower bound
+        from.date = new Date(Math.max(since, from.date.getTime()))
+      else if since
+        # choose the lower bound
+        from.date = new Date(since)
+      else if to
+        # pick the default or an 89 day window
+        from.date = getDefault(opts.defaults and opts.defaults.from, to: to.date) or
+          wesabe.lang.date.add(to.date, -89 * wesabe.lang.date.DAYS)
+
+      log.info "Adjusting date lower bound: ", from.date
+
+
+  nextAccount: ->
+    delete tmp.account
+    reload()
+
+
+  skipAccount: (args...) ->
+    wesabe.warn(args...) if args.length
+    delete @tmp.account
+
+  actionTimeoutDuration: 60000 # 1m
+  globalTimeoutDuration: 300000 # 5m
+  securityTimeoutDuration: 180000 # 3m
+
+  setErrorTimeout: (type) ->
+    duration = @["#{type}TimeoutDuration"]
+    tt = @_timeouts
+    tt ||= @_timeouts = {}
+
+    @clearErrorTimeout type
+
+    wesabe.debug "Timeout ", type, " set (",duration,")"
+
+    tt[type] = setTimeout =>
+      wesabe.trigger this, 'timeout', [type]
+      return if @job.done
+      wesabe.error "Timeout ",type," (",duration,") reached, abandoning job"
+      wesabe.tryCatch "Player#setErrorTimeout(page dump)", =>
+        @page?.dumpPrivately()
+      @job.fail 504, "timeout.#{type}"
+    , duration
+
+  clearErrorTimeout: (type) ->
+    if @_timeouts?[type]
+      wesabe.debug "Timeout ", type, " cleared"
+      clearTimeout @_timeouts[type]
+
+  onDocumentLoaded: (browser, page) ->
+    return if @job.done or @job.paused
+
+    module = @constructor.fid
+
+    # log when alert and confirm are called
+    new wesabe.dom.Bridge page.proxyTarget, ->
+      @evaluate ->
+        # evaluated on the page
+        window.alert = (message) ->
+          callback 'alert', message
+          return true
+        window.confirm = (message) ->
+          callback 'confirm', message
+          return true
+        window.open = (url) ->
+          callback 'open', url
+          return false
+
+      , (data) ->
+        # evaluated here
+          unless data
+            wesabe.debug "Bridge connected"
+            return
+
+          [type, message] = data
+
+          switch type
+            when 'alert'
+              wesabe.info type, ' called with message=', wesabe.util.inspectForLog(message)
+
+            when 'confirm'
+              wesabe.info type, ' called with message=', wesabe.util.inspectForLog(message), ', automatically answered YES'
+
+            when 'open'
+              wesabe.info type, ' called with url=', wesabe.util.inspectForLog(message)
+
+          callbacks = @["#{type}ReceivedCallbacks"]
+          if callbacks
+            for callback in callbacks
+              wesabe.lang.func.callWithScope callback, this,
+                message: message
+                browser: browser
+                page: page
+                e: @constructor.elements
+                answers: @answers
+                options: @job.options
+                log: wesabe
+                tmp: @tmp
+                action: @getActionProxy browser, page
+                job: @getJobProxy()
+                reload: => @triggerDispatch browser, page
+                skipAccount: @skipAccount
+                download: (args...) => @download args...
+              , [message]
+
+    unless @shouldDispatch browser, page
+      wesabe.info 'skipping document load'
+      return
+
+    @triggerDispatch browser, page
+
+  triggerDispatch: (browser, page) ->
+    module = @constructor.fid
+
+    browser = browser or @browser
+    page = page or @page
+
+    url = wesabe.taint(page.defaultView.location.href)
+    title = wesabe.taint(page.title)
+
+    wesabe.info 'url=', url
+    wesabe.info 'title=', title
+
+    wesabe.trigger this, 'page-load', [browser, page]
+
+    # these should not be used inside the FI scripts
+    @browser = browser
+    @page = page
+
+    @job.timer.start 'Sleep', overlap: false
+
+    setTimeout =>
+      return if @job.done or @job.paused
+
+      for dispatch in @dispatches
+        result = wesabe.tryThrow "#{module}#dispatch(#{dispatch.name})", (log) =>
+          @job.timer.start 'Dispatch', overlap: false
+
+          wesabe.lang.func.callWithScope dispatch.callback, this,
+            browser: browser
+            page: page
+            e: @constructor.elements
+            answers: @answers
+            options: @job.options
+            log: log
+            tmp: @tmp
+            action: @getActionProxy browser, page
+            job: @getJobProxy()
+            reload: => @triggerDispatch browser, page
+            skipAccount: @skipAccount
+            download: (args...) => @download args...
+
+        if result is false
+          wesabe.info "dispatch chain halted"
+          return
+    , 2000
+
+  onDownloadSuccessful: (browser, page) ->
+    for callback in @afterDownloadCallbacks
+      @runAction callback, browser, page
+
+  shouldDispatch: (browser, page) ->
+    for filter in @filters
+      result = wesabe.tryCatch "#{@constructor.fid}#filter(#{filter.name})", (log) =>
+        r = wesabe.lang.func.callWithScope filter.test, this,
+          browser: browser
+          page: page
+          e: @constructor.elements
+          log: log
+          tmp: @tmp
+          job: @getJobProxy()
+          skipAccount: @skipAccount
+
+        if r is true
+          log.debug "forcing dispatch"
+        else if r is false
+          log.debug "aborting dispatch"
+
+        return r
+
+      # check for a definite answer
+      return result if wesabe.isBoolean result
+
+    wesabe.debug "no filter voted to force or abort dispatch, so forcing dispatch by default"
+    return true
+
+  @::__defineGetter__ 'history', ->
+    @_history ||= []
+
+  @::__defineGetter__ 'tmp', ->
+    @_tmp ||= {}
+
+  @build: (fid) ->
+    wesabe.tryThrow "download.Player.build(fid=#{fid})", (log) =>
+      klass = wesabe.tryThrow "loading fi-scripts.#{fid}", =>
+        wesabe.require 'fi-scripts.' + fid
+
+      new klass(fid)

 class ActionProxy
   constructor: (@player, @browser, @page) ->

   __noSuchMethod__: (method, args) ->
     @player.runAction method, @browser, @page
-


bugfix: reference error

bugfix: reference error

Missed this when I was refactoring

--- workers/social/lib/social/models/payment/product.coffee
+++ workers/social/lib/social/models/payment/product.coffee
@@ -100,7 +100,7 @@

       planData =
         planCode    : @planCode
-        title       : "#{ product.title } - Overage"
+        title       : "#{ @title } - Overage"
         feeAmount   : @feeAmount
         feeInterval : switch @subscriptionType
           when 'recurring' then 1


Prevent style caching if the value is an empty string

--- lib/mixins/dom-styles-reader.coffee
+++ lib/mixins/dom-styles-reader.coffee
@@ -36,7 +36,8 @@
     value = getComputedStyle(parent).getPropertyValue(property)
     @dummyNode.innerHTML = ''

-    @constructor.domStylesCache[key][property] = value
+    @constructor.domStylesCache[key][property] = value unless value is ""
+    value

   # Internal: Creates a DOM node container for all the operations that
   # need to read styles properties from DOM.


Removes the default config of a plugin on unregistration

--- lib/mixins/plugin-management.coffee
+++ lib/mixins/plugin-management.coffee
@@ -17,4 +17,5 @@
     else
       plugin.deactivatePlugin()
   unregisterPlugin: (name) ->
+    delete @configDefault.plugins[name]
     delete @plugins[name]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/follow.coffee
+++ src/follow.coffee
@@ -2,7 +2,7 @@
 The Follow module provides a simple method to set an object's
 direction so that it is pointed at another object.

-The calculated velocity is based on the center point of
+The calculated direction is based on the center point of
 each object.

 This method relies on both objects having `position` methods.