add integration tests for factory objects and arrays

--- test/integration.coffee
+++ test/integration.coffee
@@ -280,83 +280,51 @@
       test.equal a, value
       test.done()

-#   'promises awaiting resolution are not cached and reused with nocache': (test) ->
-#     test.expect 7
-#     lifetime =
-#       factories:
-#         a: ->
-#           # here a new object is created
-#           Promise.delay {}, 10
-#
-#     lifetime.factories.a.$nocache = true
-#
-#     p1 = hinoki(lifetime, 'a')
-#     test.ok not lifetime.promisesAwaitingResolution?.a?
-#     # the first promise has one step more which handles caching
-#     # and cleanup of promise caching
-#     p2 = hinoki(lifetime, 'a')
-#     test.ok not lifetime.promisesAwaitingResolution?.a?
-#     p3 = hinoki(lifetime, 'a')
-#     test.ok not lifetime.promisesAwaitingResolution?.a?
-#
-#     Promise.all([p1, p2, p3]).then ([a1, a2, a3]) ->
-#       # three different objects!
-#       test.equal 'object', typeof a1
-#       test.notEqual a1, a2
-#       test.notEqual a2, a3
-#       test.ok not lifetime.promisesAwaitingResolution?.a?
-#       test.done()
-#
-#   'a factory with $nocache property is not cached': (test) ->
-#     lifetime =
-#       factories:
-#         x: -> 1
-#
-#     lifetime.factories.x.$nocache = true
-#
-#     hinoki(lifetime, 'x').then (x) ->
-#       test.equal x, 1
-#       test.ok not lifetime.values?
-#       test.done()
-#
-#   'single factory source function': (test) ->
-#     lifetime =
-#       factories: (name) ->
-#         switch name
-#           when 'a' then -> 0
-#           when 'b' then -> 1
-#           when 'c' then (a, b) -> a + b
-#           when 'd' then (b, c) -> b + c
-#
-#     hinoki(lifetime, 'd')
-#       .then (d) ->
-#         test.equal d, 2
-#         hinoki(lifetime, 'e')
-#       .catch hinoki.UnresolvableError, (error) ->
-#         test.equal error.message, "unresolvable name 'e' (e)"
-#         test.done()
-#
-#   'multiple factory sources': (test) ->
-#     lifetime =
-#       factories: [
-#         (name) ->
-#           if name is 'd'
-#             (b, c) -> b + c
-#         {
-#           a: -> 0
-#         }
-#         (name) ->
-#           if name is 'b'
-#             -> 1
-#         {
-#           c: (a, b) -> a + b
-#         }
-#       ]
-#
-#     hinoki(lifetime, 'd')
-#       .then (d) ->
-#         test.equal d, 2
-#         hinoki(lifetime, 'e')
-#       .catch hinoki.UnresolvableError, (error) ->
-#         test.equal error.message, "unresolvable name 'e' (e)"
-#         test.done()
+  'lifetime can be omitted': (test) ->
+    source = hinoki.source
+      a: -> 1
+      b: (a) -> a + 1
+      c: (b) -> b + 1
+    hinoki source, (c) ->
+      test.equal c, 3
+      test.done()
+
+  'factory objects': (test) ->
+    source = hinoki.source
+      alpha:
+        bravo: (bravo) -> bravo
+        charlie: (charlie) -> charlie
+        delta: (delta) -> delta
+      bravo: -> 'bravo'
+      charlie: -> 'charlie'
+      delta: -> 'delta'
+
+    lifetime = {}
+
+    hinoki source, lifetime, (alpha) ->
+      test.deepEqual alpha,
+        bravo: 'bravo'
+        charlie: 'charlie'
+        delta: 'delta'
+      test.done()
+
+  'factory arrays': (test) ->
+    source = hinoki.source
+      alpha: [
+        (bravo) -> bravo
+        (charlie) -> charlie
+        (delta) -> delta
+      ]
+      bravo: -> 'bravo'
+      charlie: -> 'charlie'
+      delta: -> 'delta'
+
+    lifetime = {}
+
+    hinoki source, lifetime, (alpha) ->
+      test.deepEqual alpha, [
+        'bravo'
+        'charlie'
+        'delta'
+      ]
+      test.done()


Correct internal documentation about what loadModel returns.

--- vendor/assets/javascripts/brainstem/storage-manager.coffee
+++ vendor/assets/javascripts/brainstem/storage-manager.coffee
@@ -49,11 +49,12 @@
   setErrorInterceptor: (interceptor) ->
     @errorInterceptor = interceptor || (handler, modelOrCollection, options, jqXHR, requestParams) -> handler?(jqXHR)

-  # Request a model to be loaded, optionally ensuring that associations be included as well.  A model is returned immediately and is updated
-  # when the load, and any dependent loads, are complete.
-  #     model = manager.loadModel "time_entry", 2
-  #     model = manager.loadModel "time_entry", 2, fields: ["title", "notes"]
-  #     model = manager.loadModel "time_entry", 2, include: ["project", "task"]
+  # Request a model to be loaded, optionally ensuring that associations be included as well.  A loader (which is a jQuery promise) is returned immediately and is resolved
+  # with the model from the StorageManager when the load, and any dependent loads, are complete.
+  #     loader = manager.loadModel "time_entry", 2
+  #     loader = manager.loadModel "time_entry", 2, fields: ["title", "notes"]
+  #     loader = manager.loadModel "time_entry", 2, include: ["project", "task"]
+  #     manager.loadModel("time_entry", 2, include: ["project", "task"]).done (model) -> console.log model
   loadModel: (name, id, options = {}) ->
     return if not id


Social: fix send data.account instead of accountId

--- workers/social/lib/social/models/socialapi/requests.coffee
+++ workers/social/lib/social/models/socialapi/requests.coffee
@@ -180,7 +180,7 @@
   # make the object according to channel participant data
   req = ({accountId} for accountId in data.accountIds)

-  url = "#{url}?accountId=#{accountId}"
+  url = "#{url}?accountId=#{data.accountId}"
   post url, req, callback

 updateLastSeenTime = (data, callback)->


Join patters: functions when and update, and composite unsubscribe

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -138,38 +138,8 @@
     streams = [streams].concat(more)
   Bacon.zipWith(streams, Array)

-Bacon.zipWith = (streams, f, more...) ->
-    if isFunction(streams)
-      g = streams
-      streams = [f].concat(more)
-      f = g
-    new EventStream (sink) ->
-      bufs = ([] for s in streams)
-      unsubscribed = false
-      unsubs = (nop for s in streams)
-      unsubAll = (-> f() for f in unsubs ; unsubscribed = true)
-      zipSink = (e) ->
-        reply = sink e
-        if reply == Bacon.noMore or e.isEnd()
-          unsubAll()
-        reply
-      handle = (i) -> (e) ->
-       if e.isError()
-         zipSink e
-       else if e.isInitial()
-         Bacon.more
-       else
-         bufs[i].push(e)
-         if not e.isEnd() and _.all(b.length for b in bufs)
-           vs = (b.shift().value() for b in bufs)
-           reply = zipSink e.apply _.always f(vs ...)
-         if _.any(b.length and b[0].isEnd() for b in bufs)
-           reply = zipSink end()
-         reply or Bacon.more
-      for s,j in streams
-        unsubs[j] = do (i=j) ->
-          s.subscribe (handle i) unless unsubscribed
-      unsubAll
+Bacon.zipWith = (streams, f) ->
+  Bacon.when streams, f

 Bacon.combineAsArray = (streams, more...) ->
   if not (streams instanceof Array)
@@ -961,6 +931,117 @@
       unsubAll()
       sink? end()

+Bacon.when = (patterns...) ->
+    return Bacon.never() if patterns.length == 0
+    len = patterns.length
+    usage = "when: expecting arguments on the form (Observable+,function)+"
+    assert usage, len % 2 == 0
+    sources = []
+    pats = []
+    i = 0
+    while (i < len)
+       patterns[i] = [patterns[i]] unless patterns[i] instanceof Array
+       assert (p instanceof Observable), usage for p in patterns[i]
+       patterns[i+1] = (-> patterns[i+1]) unless patterns[i+1] instanceof Function
+       pat = {f: patterns[i+1], ixs: []}
+       for s in patterns[i]
+         index = sources.indexOf s
+         if index < 0
+            sources.push(s)
+            index = sources.length - 1
+         (ix.count++ if ix.index == index) for ix in pat.ixs
+         pat.ixs.push {index: index, count: 1}
+       pats.push pat
+       i = i + 2
+
+    class src
+      constructor: (s) ->
+        @obs = s
+        @queue = []
+        @isEnded = false
+        if s instanceof Property
+          @value = ()  -> @queue[0]
+          @push  = (x) -> @queue = [x]
+          @ended = ()  -> false
+        else
+          @value = ()  -> @queue.shift()
+          @push  = (x) -> @queue.push(x)
+          @ended = (c) -> @queue.length < c && @isEnded
+
+    sources = _.map ((s) -> new src(s)), sources
+
+    fromStreams = new EventStream (sink) ->
+      match = (p) ->
+        _.all(p.ixs, (i) -> sources[i.index].queue.length >= i.count)
+      cannotMatch = (p) ->
+        _.any(p.ixs, (i) -> sources[i.index].ended(i.count))
+      part = (source, j) -> (unsubAll) ->
+        source.obs.subscribe (e) ->
+          if e.isEnd()
+            sources[j].isEnded = true
+            if _.all(pats, cannotMatch)
+              reply = Bacon.noMore
+              sink end()
+          else if e.isError()
+            reply = sink e
+          else
+            sources[j].push e.value()
+            for p in pats
+               if match(p)
+                 val = p.f(sources[i.index].value() for i in p.ixs ...)
+                 reply = sink next(val)
+                 break;
+          unsubAll() if reply == Bacon.noMore
+          reply or Bacon.more
+
+      compositeUnsubscribe (part s,i for s,i in sources)...
+
+Bacon.update = (initial, patterns...) ->
+  lateBindFirst = (f) -> (args) -> (i) -> f([i].concat(args)...)
+  i = patterns.length - 1
+  while (i > 0)
+    unless patterns[i] instanceof Function
+      patterns[i] = do(x=patterns[i])->(->x)
+    patterns[i] = lateBindFirst patterns[i]
+    i = i - 2
+  Bacon.when(patterns...).scan initial, ((x,f) -> f x)
+
+compositeUnsubscribe = (ss...) ->
+  new CompositeUnsubscribe(ss).unsubscribe
+
+class CompositeUnsubscribe
+  constructor: (ss = []) ->
+    @unsubscribed = false
+    @subscriptions = []
+    @starting = []
+    @add s for s in ss
+  add: (subscription) =>
+    return if @unsubscribed
+    ended = false
+    unsub = nop
+    @starting.push subscription
+    unsubMe = =>
+      unsub()
+      ended = true
+      @remove unsub
+      _.remove subscription, @starting
+    unsub = subscription @unsubscribe, unsubMe
+    @subscriptions.push unsub unless (@unsubscribed or ended)
+    _.remove subscription, @starting
+    unsub
+  remove: (subscription) ->
+    return if @unsubscribed
+    _.remove subscription, @subscriptions
+  unsubscribe: =>
+    return if @unsubscribed
+    @unsubscribed = true
+    s() for s in @subscriptions
+    @subscriptions = []
+    @starting = []
+  empty: =>
+    not @subscriptions.length and not @starting.length
+
+
 class Some
   constructor: (@value) ->
   getOrElse: -> @value


Add test for datefirst set by config

Add test for datefirst set by config

--- test/integration/connection-test.coffee
+++ test/integration/connection-test.coffee
@@ -1132,5 +1132,29 @@
   connection.on 'end', (info) ->
     test.done()

-# Test that the DATEFISRT setting can be changed via an optional configuration
-
+# Test that the DATEFIRST setting can be changed via an optional configuration
+exports.testDatefirstDefault = (test) ->
+  test.expect(3)
+
+  config = getConfig()
+  config.options.datefirst = 3
+
+  connection = new Connection(config)
+
+  request = new Request('select @@datefirst', (err) ->
+    test.ifError(err)
+    connection.close()
+  )
+
+  request.on('row', (columns) ->
+    dateFirst = columns[0].value
+    test.strictEqual(dateFirst, 3)
+  )
+
+  connection.on 'connect', (err) ->
+    test.ifError(err)
+    connection.execSql(request)
+
+  connection.on 'end', (info) ->
+    test.done()
+


Improved a comment.

Improved a comment.

--- server.coffee
+++ server.coffee
@@ -51,7 +51,7 @@

         s = {}
         if @inArray
-          # We have cannot use top-level $or with $elemMatch
+          # We have cannot use top-level $or with $elemMatch so we do it differently than for $and below
           # See: https://jira.mongodb.org/browse/SERVER-11537

           selector[@ancestorArray] ?= {}


insert takeaways

insert takeaways

--- server.coffee
+++ server.coffee
@@ -21,7 +21,7 @@
                        })
         else
           ta_id = ta._id
-
+
         new_dish = {
           name:         dish.name,
           description:  dish.description,
@@ -30,4 +30,4 @@
         };

         # ...and add the dish-object to the takeaway's dishes
-        share.Takeaways.update(ta_id, {$push: {dishes: new_dish}});
+        share.Takeaways.update({_id: ta_id}, {$push: {dishes: new_dish}});


Don't show sidebar menu when not signed in

--- core/app/backbone/controllers/channels_controller.coffee
+++ core/app/backbone/controllers/channels_controller.coffee
@@ -7,9 +7,11 @@

   showSidebarForTopic: (topic) ->
     FactlinkApp.leftBottomRegion.close()
-    @showUserProfile currentUser
-    window.Channels.setUsernameAndRefreshIfNeeded currentUser.get('username') # TODO: check if this can be removed
-    FactlinkApp.Sidebar.showForTopicsAndActivateCorrectItem(topic)
+
+    if Factlink.Global.signed_in
+      @showUserProfile currentUser
+      window.Channels.setUsernameAndRefreshIfNeeded currentUser.get('username') # TODO: check if this can be removed
+      FactlinkApp.Sidebar.showForTopicsAndActivateCorrectItem(topic)

   showTopicFacts: (slug_title) ->
     FactlinkApp.mainRegion.close()


Handling inexistent controllers

Handling inexistent controllers

--- www/src/theoricus/core/router.coffee
+++ www/src/theoricus/core/router.coffee
@@ -109,19 +109,16 @@
       controller_name = url_parts[0]
       action_name = url_parts[1] or 'index'

-      @the.factory.controller controller_name, (controller)=>
+      try
+        Controller = require.resolve 'app/controllers/' + controller_name
+        route = @map url, "#{controller_name}/#{action_name}", null, 'body'
+        return @on_change route, url

-        # if controller is found, route call to it
-        if controller?
-          route = @map url, "#{controller_name}/#{action_name}", null, 'body'
-          return @on_change route, url
-
-        # otherwise renders the not found route
-        else
-
-          for route in @routes
-            if route.test @Routes.notfound
-              return @on_change route, url
+      # otherwise renders the not found route
+      catch e
+        for route in @routes
+          if route.test @Routes.notfound
+            return @on_change route, url

     @trigger = true


Added || false for if the option is undefined

--- core/app/backbone/views/share/share_new_fact_view.coffee
+++ core/app/backbone/views/share/share_new_fact_view.coffee
@@ -11,7 +11,7 @@

   onRender: ->
     @$el.addClass "factlink-icon-#{@options.name}"
-    @$el.toggleClass 'share-button-checked', @model.get(@options.name)
+    @$el.toggleClass 'share-button-checked', @model.get(@options.name) || false

   toggleChecked: ->
     @model.set @options.name, !@model.get(@options.name)


Extract links, texts, images and their visual hints.

--- extractor/extractor.coffee
+++ extractor/extractor.coffee
@@ -1,4 +1,4 @@
-url = 'http://www.theverge.com/2013/8/6/4594158/fitness-tracker-quantified-self-workout'
+#url = 'http://www.theverge.com/2013/8/6/4594158/fitness-tracker-quantified-self-workout'
 #url = 'http://www.amazon.com/gp/product/B004X1V1CS/ref=s9_simh_gw_p351_d0_i2?pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-2&pf_rd_r=1AZ2019VKPDJGBSYZG8T&pf_rd_t=101&pf_rd_p=1389517282&pf_rd_i=507846'
 #url = 'http://news.163.com/13/0807/09/95LRCHP20001124J.html'
 #url = 'http://webdesign.tutsplus.com/tutorials/htmlcss-tutorials/the-role-of-table-layouts-in-responsive-web-design/'
@@ -7,23 +7,53 @@
 #url = 'http://localhost:8888/'
 #url = 'http://www.wired.com/business/2013/06/meditation-mindfulness-silicon-valley/'

+system = require('system')
 page = require('webpage').create()

+page.viewportSize =
+    width: 1600
+    height: 4000
+
 page.onResourceRequested = (request) ->
-    #console.log 'Request ' + JSON.stringify request, undefined, 2
+    system.stderr.write JSON.stringify request, undefined, 2
+    system.stderr.write '\n\n'

 page.onResourceReceived = (response) ->
-    #console.log 'Receive ' + JSON.stringify response, undefined, 2
+    system.stderr.write JSON.stringify response, undefined, 2
+    system.stderr.write '\n\n'

-page.open url, ->
-    links = page.evaluate ->
-        (link.href for link in document.querySelectorAll('a[href]'))
+page.onConsoleMessage = (message) ->
+    system.stderr.write message
+    system.stderr.write '\n\n'

-    nodes = page.evaluate ->
-        try
-            String.prototype.trim = -> @replace /^\s+|\s+$/g, ''
+page.onLoadFinished = (status) ->
+    # bail on network issue
+    return phantom.exit() if status is not 'success'

-            build_tag_path = (element) ->
+    # inject javascripts helpers needed
+    page.evaluate ->
+        String.prototype.trim = -> @replace /^\s+|\s+$/g, ''
+
+        ((window) ->
+            'use strict'
+
+            window.__slice = [].slice
+
+            namespace = (target, name, block) ->
+                [target, name, block] = [window, arguments...] if arguments.length < 3
+                top = target
+                target = target[item] or= {} for item in name.split '.'
+                block target, top
+
+            namespace 'spider', (exports, top) ->
+                exports.namespace = namespace
+
+        )(window)
+
+        spider.namespace 'spider.utils', (exports) ->
+            'use strcit'
+
+            exports.path = (element) ->
                 path = []
                 while element
                     name = element.tagName.toLowerCase()
@@ -37,66 +67,117 @@
                     element = element.parentElement
                 return path

-            nodes = []
+            exports.bound = (element) ->
+                scrollTop = document.documentElement.scrollTop or document.body.scrollTop
+                scrollLeft = document.documentElement.scrollLeft or document.body.scrollLeft
+                rect = element.getBoundingClientRect()
+                bound =
+                    width: rect.width
+                    height: rect.height
+                    left: rect.left + scrollLeft
+                    top: rect.top + scrollTop
+                return bound

-            # walk over all text in the page
-            walker = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT, null, false
-            while text = walker.nextNode()
-                continue unless text.nodeValue.trim().length > 0
+            exports.computed = (element) ->
+                computed = document.defaultView.getComputedStyle(element)
+                data =
+                    width: computed.width
+                    height: computed.height
+                    color: computed.color
+                    lineHeight: computed.lineHeight
+                    fontSize: computed.fontSize
+                    fontWeight: computed.fontWeight
+                    fontFamily: computed.fontFamily
+                    fontStyle: computed.fontStyle
+                    opacity: computed.opacity
+                return data

-                # container node
-                node = text.parentElement
-                continue unless node.offsetWidth * node.offsetHeight > 0
+    # extract basic data
+    data = page.evaluate ->
+        data =
+            url: window.location.href
+            body:
+                scroll:
+                    top: document.documentElement.scrollTop or document.body.scrollTop
+                    left: document.documentElement.scrollLeft or document.body.scrollLeft
+                bound: spider.utils.bound(document.body)
+                computed: spider.utils.computed(document.body)
+        return data

-                # find the parent node that is a block
-                while node
-                    computed = document.defaultView.getComputedStyle(node)
-                    width = parseInt(computed.width)
-                    height = parseInt(computed.height)
-                    break if width * height > 0
-                    node = node.parentElement
-                continue unless node
+    # extract links
+    links = page.evaluate ->
+        (link.href for link in document.querySelectorAll('a[href]'))

-                # have we seen this node?
-                if node.spider
-                    node.spider.text.push text.nodeValue
-                    continue
+    # extract data
+    texts = page.evaluate ->
+        texts = []

-                # collect features
-                path = build_tag_path(node)
+        # walk over all text in the page
+        walker = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT, null, false
+        while text = walker.nextNode()
+            continue unless text.nodeValue.trim().length > 0
+
+            # container node
+            node = text.parentElement
+            bound = spider.utils.bound(node)
+            continue unless bound.width * bound.height > 0
+
+            # find the parent node that is a block
+            while node
                 computed = document.defaultView.getComputedStyle(node)
+                break if parseInt(computed.width) * parseInt(computed.height) > 0
+                node = node.parentElement
+            continue unless node

-                node.spider =
-                    node: path[path.length - 1]
-                    path: path
-                    text: [text.nodeValue]
-                    html: node.innerHTML
-                    offset:
-                        width: node.offsetWidth
-                        height: node.offsetHeight
-                        top: node.offsetTop
-                        left: node.offsetLeft
-                    computed:
-                        width: computed.width
-                        height: computed.height
-                        color: computed.color
-                        lineHeight: computed.lineHeight
-                        fontSize: computed.fontSize
-                        fontWeight: computed.fontWeight
-                        fontFamily: computed.fontFamily
-                        fontStyle: computed.fontStyle
-                        opacity: computed.opacity
-                nodes.push node.spider
+            # have we seen this node?
+            if node.spider
+                node.spider.text.push text.nodeValue
+                continue

-                node.style.border = '1px solid red'
+            # collect features
+            path = spider.utils.path(node)

-            return nodes
+            node.spider =
+                element: path[path.length - 1]
+                path: path
+                text: [text.nodeValue]
+                html: node.innerHTML
+                bound: spider.utils.bound(node)
+                computed: spider.utils.computed(node)
+            texts.push node.spider

-        catch e
-            return e
+            # debug
+            node.style.border = '1px solid red'

+        return texts
+
+    # extract images
+    images = page.evaluate ->
+        images = []
+        for image in document.querySelectorAll('img[src]')
+            bound = spider.utils.bound(image)
+            continue unless bound.width * bound.height > 0
+            path = spider.utils.path(image)
+            images.push
+                src: image.src
+                element: path[path.length - 1]
+                path: path
+                bound: bound
+                computed: spider.utils.computed(image)
+        return images
+
+    data.links = links
+    data.texts = texts
+    data.images = images
+
+    # debug
+    console.log JSON.stringify data, undefined, 2
+    #console.log JSON.stringify data
+
+    # debug
     page.render('test.jpg')

-    console.log JSON.stringify nodes, undefined, 2
+    phantom.exit()

-    phantom.exit()
+# load page
+page.open system.args[1]


Extracting ajaxRequest into a class method to allow for easier re-use

--- src/api.coffee
+++ src/api.coffee
@@ -26,6 +26,7 @@
   # Construct a new API binding
   #
   #  authToken: The authentication token with which we can access the DNAnexus API.
+  #
   # options:
   #   apiServer: Override the API server information. Default: "api.dnanexus.com"
   #   maxAJAXTrials: The number of times to retry an AJAX request. Default: 5
@@ -52,14 +53,16 @@
   #   dataType: Passed straight through to jquery ajax if using jquery, else ignored
   #   skipConversion: If true, object data will not be converted to JSON
   #   withCredentials: A boolean indicating whether or not the with credentials xhrFlag should be set. Default is false
+  #   maxRetries: The maximum number of times to retry the request if it fails due to a network error. Default is 5.
   #
   # Returns a deferred object with has an abort method added
   #
-  _ajaxRequest: (url, options = {}, trial = 0) ->
+  @_ajaxRequest: (url, options = {}, trial = 0) ->
     status = $.Deferred()
     headers = options.headers ? {}
     data = options.data
     method = options.method ? "POST"
+    maxRetries = options.maxRetries ? 5

     request = null

@@ -94,7 +97,7 @@
             # TODO: Notify the client that the API server is down temporarily

           else if textStatus == "error" && jqXHR.status == 0
-            if trial > @maxAJAXTrials
+            if trial > maxRetries
               rejectStatus({ type: "AjaxError", details: {jqXHR: jqXHR}})
             else
               # Backoff/Retry
@@ -184,11 +187,12 @@
     input =
       headers: headers
       data: input
+      maxRetries: @maxAJAXTrials

     if options.withCredentials == true
       input.withCredentials = true

-    ajaxRequest = @._ajaxRequest(url, input)
+    ajaxRequest = Api._ajaxRequest(url, input)

     # Decorate the deferred object with an abort method which cancels the ajax request and sets the internal
     # state of the deferred object to aborted, which prevents the deferred object from being resolved
@@ -291,5 +295,4 @@

     return deferred

-
 module.exports = Api


call bodyParser methods separately

call bodyParser methods separately

--- src/app.coffee
+++ src/app.coffee
@@ -81,7 +81,7 @@
 		udhcpd.enableAsync(options)

-openHotspot = (wifi, ssid, passphrase) ->
+openHotspot = (wifi, ssid, passphrase) ->
 	systemd.stop('wpa_supplicant')
 	.delay(3000)
 	.then ->
@@ -167,7 +167,11 @@
 		wifi = Promise.promisifyAll(wifi)
 		console.log('WiFi initialized')

-		app.use(bodyParser())
+		app.use(bodyParser.urlencoded({
+		  extended: true
+		}))
+		app.use(bodyParser.json())
+
 		app.use(express.static(__dirname + '/public'))
 		app.get '/ssids', (req, res) ->
 			res.send(ssidList)


fix #30: reload page on hash change

fix #30: reload page on hash change

--- src/app.coffee
+++ src/app.coffee
@@ -27,6 +27,8 @@
         notebook.run_cell rc_cell
         notebook.remove_cell rc_cell

+      window.onhashchange = -> window.location.reload()
+
       uri = URI location.href
       fragment = uri.fragment()
       if fragment.length > 0 and fragment[0] == '/'


...print?

...print?

--- src/app.coffee
+++ src/app.coffee
@@ -329,12 +329,6 @@

 navTools = [
     new app.models.NavToolModel
-      id: "addLayer"
-      toolName: "Add Layer"
-      modalName: "Add a Layer"
-      modalBody: "Not Implemented Yet"
-  ,
-    new app.models.NavToolModel
       id: "print"
       toolName: "Print a Map"
       modalName: "Print a Map"


Minor log change

Minor log change

--- src/app.coffee
+++ src/app.coffee
@@ -35,6 +35,6 @@
 		console.log((prefix + data).trim().replace(/\n/gm, '\n#{prefix}'))
 	)

-	console.log('Start API server')
+	console.log('Starting API server..')
 	api.listen(80)
 )


Add language-entries filter

Add language-entries filter

--- src/app.coffee
+++ src/app.coffee
@@ -1,5 +1,11 @@
-endpointBase = "http://api.c4w.jp/api/v1/"
-getEndpoint = (key) -> "#{endpointBase}#{key}.json"
+ENDPOINT_BASE = "http://api.c4w.jp/api/v1/"
+DEFAULT_LANG = 'ja'
+FALLBACK_LANG = 'en'
+LANGUAGE_LABELS =
+    ja: '日本語'
+    en: 'English'
+
+getEndpoint = (key) -> "#{ENDPOINT_BASE}#{key}.json"

 app = angular.module 'disaster-information-client', [
     'pascalprecht.translate'
@@ -16,8 +22,8 @@
             suffix: '.json'
         }
         $translateProvider.useSanitizeValueStrategy null
-        $translateProvider.preferredLanguage 'ja'
-        $translateProvider.fallbackLanguage 'ja'
+        $translateProvider.preferredLanguage DEFAULT_LANG
+        $translateProvider.fallbackLanguage FALLBACK_LANG
 ]

@@ -27,16 +33,13 @@
     '$rootScope'
     (acquireLocales, acquireEntries, $rootScope) ->
         # setting
-        $rootScope.languageLabel = {
-            ja: '日本語'
-            en: 'English'
-        }
+        $rootScope.languageLabel = LANGUAGE_LABELS
         $rootScope.err = false

         acquireLocales()
             .then (locales) ->
                 $rootScope.locales = locales
-                $rootScope.selectedLocale = locales[0]
+                $rootScope.selectedLocale = DEFAULT_LANG
                 $rootScope.entries = []
                 return locales

@@ -45,11 +48,14 @@

             .then (entries) ->
                 $rootScope.entries = []
+                console.log entries
                 # sort and flatten
-                desc = (a, b) -> Date.parse(b.date) - Date.parse(a.date)
                 entries
-                    .sort desc
-                    .forEach (obj)->
+                    .sort (a, b) ->
+                        date = (x) ->
+                            Date.parse x.date
+                        return date b - date a
+                    .forEach (obj) ->
                         obj.forEach (entry) ->
                             $rootScope.entries.push entry

@@ -97,7 +103,6 @@
                                 entry.date = new Date(rawDate).toLocaleDateString()
                                 entry.time = new Date(rawDate).toLocaleTimeString()
                                 entry.body = entry.body # need sanitization?
-                                entry.locale = locale
                         deferred.resolve entries
                         return deferred.promise

@@ -168,16 +173,16 @@
         replace: true
         templateUrl: 'templates/language-switch.html'
         controller: [
+            '$translate'
             '$scope'
             '$rootScope'
-            '$translate'
-            ($scope, $rootScope, $translate) ->
+            ($translate, $scope, $rootScope) ->

                 changeLanguage = (key) ->
                     $translate.use(key)
                     $rootScope.selectedLocale = key
                     $rootScope.entriesShim = $rootScope.entries.filter (entry) ->
-                        entry.locale = key
+                        entry.lang is key

                 $rootScope.$on 'entriesLoaded', ->
                     $scope.key = $rootScope.selectedLocale


Fixed popups to display over the whole checkbox, not just the input. Added support for using "description" for schema fields when creating popups.

--- src/Forms.coffee
+++ src/Forms.coffee
@@ -1,6 +1,4 @@
 Forms =
-
-  FIELD_SELECTOR: '.form-group',

   defineModelForm: (formArgs) ->
     name = formArgs.name
@@ -208,8 +206,11 @@
       hasRequiredField = false
       _.each schemaInputs, (input, key) ->
         $input = $(input.node)
+        if Forms.isCheckbox($input)
+          # Ensure the tooltip is available on the label as well as the input.
+          $input = $input.parent()
         field = input.field
-        desc = field.desc
+        desc = field.desc ? field.description
         # Add popups to the inputs contain definitions from the schema.
         if desc?
           popupInputs.push($input.data('desc', desc))


Text example it added.

Text example it added.

--- examples/test.coffee
+++ examples/test.coffee
@@ -10,6 +10,12 @@
 			voice = null

 			Given "I am a good singing", ->
+				it 'should be true', ->
+					true.should.eql true
+
+				it 'should be true too', ->
+					true.should.eql true
+
 			When "I sing", ->
 				voice = 'good'
 			Then "it should sound good", ->


Fixed typo

Fixed typo

--- client/arbitrate/new.coffee
+++ client/arbitrate/new.coffee
@@ -1,4 +1,4 @@
-{ Crypto: convert: { bytesToBase64 } } = require 'bitcoinjs-lib'
+{ Crypto, convert: { bytesToBase64 } } = require 'bitcoinjs-lib'
 { util: { randomBytes }, charenc: { UTF8 } } = Crypto
 { random_privkey, get_address, parse_key_string, ADDR_PRIV } = require '../../lib/bitcoin/index.coffee'
 { signup } = require '../lib/user.coffee'


Disable Shred's use of cookie jar

Disable Shred's use of cookie jar

--- client/coffee/client.coffee
+++ client/coffee/client.coffee
@@ -36,6 +36,7 @@
         method: "GET"
         headers:
           "Accept": "application/json"
+        cookieJar: null
         on: handlers

   @backcompat: (service_url, callback) ->
@@ -45,6 +46,7 @@
         method: "GET"
         headers:
           "Accept": "application/json"
+        cookieJar: null
         on:
           200: (response) ->
             client = new Client(response.content.data)
@@ -170,6 +172,7 @@
         method: method
         headers: {}
         query: options.query
+        cookieJar: null
         on: {}
       if options.body
         request.body = options.body


Add socket.io chat in comments

Add socket.io chat in comments

--- client/data/cardData.coffee
+++ client/data/cardData.coffee
@@ -1,4 +1,11 @@
 module.exports = [
+#   id: 'socket.io'
+#   title: 'socket.io chat'
+#   img: 'png'
+#   description: 'Official chat demo for socket.io'
+#   url:
+#     github: 'https://github.com/Automattic/socket.io/tree/master/examples/chat'
+# ,
 #   id: 'dubhacks'
 #   title: 'Dubhacks'
 #   img: 'png'
@@ -87,4 +94,4 @@
   description: 'A simple classroom assignment manager.'
   url:
     github: 'https://github.com/grant/Acadee'
-]+]


Mount machine when workspace is ready

Mount machine when workspace is ready

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -308,7 +308,8 @@
     then "/home/#{owner}"
     else '/'

-    filesPane.emit 'MachineMountRequested', machineData, path
+    @workspace.ready ->
+      filesPane.emit 'MachineMountRequested', machineData, path

   unmountMachine: (machineData) ->


User password hash test accepts production or development salt for convenience.

--- test/server/functional/user.spec.coffee
+++ test/server/functional/user.spec.coffee
@@ -69,7 +69,7 @@
       expect(user.get('password')).toBeUndefined()
       expect(user?.get('passwordHash')).not.toBeUndefined()
       if user?.get('passwordHash')?
-        expect(user.get('passwordHash')[..5]).toBe('31dc3d')
+        expect(user.get('passwordHash')[..5] in ['31dc3d', '948c7e']).toBeTruthy()
         expect(user.get('permissions').length).toBe(0)
       done()


peillis/helpapp#3455  scroll widget test !!!

--- test/test_widget.coffee
+++ test/test_widget.coffee
@@ -269,3 +269,36 @@
       # search and start the whole thing
       queryEl.val 'sill'
       queryEl.trigger 'keydown'
+
+    it ' calls nextPage when df:scroll is triggered', (done)->
+      scrollContainer = @scrollContainer
+      scrollContainer.css position: 'relative', height: '800px', overflow: 'auto'
+      content = scrollContainer.find('div').first()
+      content.css height: '1200px'
+      controller = @controller
+      controller.addWidget @scrollWidget
+      queryEl = @queryEl
+      self = this
+      _nextPage = controller.nextPage
+      nextPageCalled = false
+
+      controller.nextPage = () ->
+        nextPageCalled = true
+        controller.nextPage = _nextPage
+        done()
+
+      scrollContainer.one 'df:scroll', ()->
+        # first time this is called, scroll is on top. no nextPage called
+        nextPageCalled.should.be.false
+        # now we move the scroll
+        scrollContainer.scrollTop 400
+        # and trigger df:scroll again. this time, nextPage should be called
+        scrollContainer.trigger 'df:scroll'
+
+      scrollContainer.on 'DOMSubtreeModified', ()->
+        # scroll is on top, no nextPage should be called after this
+        scrollContainer.trigger 'df:scroll'
+
+      # start the process
+      queryEl.val 'zill'
+      queryEl.trigger 'keydown'


fix modal dismiss onclick

fix modal dismiss onclick

--- resources/assets/lib/modal.coffee
+++ resources/assets/lib/modal.coffee
@@ -61,8 +61,18 @@
     @props.onClose?() if e.keyCode == 27

+  handleMouseDown: (e) =>
+    @clickStartTarget = e.target
+
+
+  handleMouseUp: (e) =>
+    @clickEndTarget = e.target
+
+
   hideModal: (e) =>
-    @props.onClose?() if e.button == 0 && e.target == @ref.current
+    @props.onClose?() if e.button == 0 &&
+                         e.target == @ref.current &&
+                         @clickEndTarget == @clickStartTarget

   open: ->
@@ -80,5 +90,7 @@
     div
       className: 'js-react-modal'
       onClick: @hideModal
+      onMouseDown: @handleMouseDown
+      onMouseUp: @handleMouseUp
       ref: @ref
       @props.children


Renamed some snippets

Renamed some snippets

--- snippets/language-sqf.cson
+++ snippets/language-sqf.cson
@@ -1,7 +1,7 @@
 '.source.sqf':
   # BI Commands - Conditional Structures
-  'if statement':
-    'prefix': 'if'
+  'If then statement':
+    'prefix': 'if_then'
     'body': """
       if (${1:CONDITION}) then {
           ${2://code}
@@ -9,8 +9,8 @@
     """
     'description': 'Conditional if statement'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/if'
-  'if-else statement':
-    'prefix': 'if-else'
+  'If-then-else statement':
+    'prefix': 'if_then_else'
     'body': """
       if (${1:CONDITION}) then {
           ${2://code}
@@ -20,13 +20,13 @@
     """
     'description': 'Conditional if-else statement'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/else'
-  'if-else assignment':
-    'prefix': 'if-else_assign'
+  'If-then-else assignment':
+    'prefix': 'if_then_else_assign'
     'body': """
       ${1:VARIABLE} = if (${2:CONDITION}) then {${3:ASSIGNMENT_TRUE}} else {${4:ASSIGNMENT_FALSE}};
     """
     'description': 'Conditional if-else assignment'
-  'switch statement':
+  'Switch statement':
     'prefix': 'switch'
     'body': """
       switch (${1:VARIABLE}) do {
@@ -35,7 +35,7 @@
     """
     'description': 'Conditional switch statement'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/switch_do'
-  'case block of switch':
+  'Case block of switch':
     'prefix': 'case_switch'
     'body': """
       case (${1:VALUE}): {
@@ -44,7 +44,7 @@
     """
     'description': 'Case block inside switch statement'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/case'
-  'default block of switch':
+  'Default block of switch':
     'prefix': 'default_switch'
     'body': """
       default {
@@ -55,7 +55,7 @@
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/default'

   # BI Commands - Loops
-  'forEach loop':
+  'ForEach Loop':
     'prefix': 'forEach'
     'body': """
       {
@@ -64,17 +64,17 @@
     """
     'description': 'Array incrementing loop'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/forEach'
-  'C-like for loop':
-    'prefix': 'for_c'
+  'For Loop (ARRAY)':
+    'prefix': 'for_array'
     'body': """
       for [{${1:BEGIN}}, {${2:CONDITION}}, {${3:STEP}}] do {
           ${4://code}
       };
     """
-    'description': 'C-like style for loop'
+    'description': 'C-like for loop'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/for_forspec'
-  'for-from-to loop':
-    'prefix': 'for_from_to'
+  'For Loop (VAR)':
+    'prefix': 'for_var'
     'body': """
       for ${1:VALUE} from ${2:STARTVALUE} to ${3:ENDVALUE} do {
           ${4://code}
@@ -82,8 +82,8 @@
     """
     'description': 'Loop from value to value'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/for_var'
-  'for-from-to-step loop':
-    'prefix': 'for_from_to_step'
+  'For Loop Step (VAR)':
+    'prefix': 'for_var_step'
     'body': """
       for ${1:VALUE} from ${2:STARTVALUE} to ${3:ENDVALUE} step ${4:STEP} do {
           ${5://code}
@@ -91,7 +91,7 @@
     """
     'description': 'Loop from value to value with custom step'
     'descriptionMoreURL': 'https://community.bistudio.com/wiki/for_var'
-  'while loop':
+  'While Loop':
     'prefix': 'while'
     'body': """
       while {${1:CONDITION}} do {
@@ -105,7 +105,7 @@
   'CBA PerFrameHandler Add':
     'prefix': 'cba_pfh_add'
     'body': """
-      [{${1:FUNCTION/CODE}}, ${2:DELAY}, [${3:PARAMETERS}]] call CBA_fnc_addPerFrameHandler;
+      [${1:FUNCTION/CODE}, ${2:DELAY}, [${3:PARAMETERS}]] call CBA_fnc_addPerFrameHandler;
     """
     'description': 'Add a handler that will execute every frame'
     'descriptionMoreURL': 'https://dev.withsix.com/docs/cba/files/common/fnc_addPerFrameHandler-sqf.html'


DomainListView: Use JDomain.fetchDomains instead

--- client/Environments/Controllers/DomainsListViewController.coffee
+++ client/Environments/Controllers/DomainsListViewController.coffee
@@ -19,7 +19,8 @@

   loadItems:(callback)->
     @showLazyLoader()
-    KD.whoami().fetchDomains (err, domains) =>
+    {JDomain} = KD.remote.api
+    JDomain.fetchDomains (err, domains) =>
       @instantiateListItems domains or []
       @hideLazyLoader()
       callback?()


activity: accept an after event for inviting others

--- client/activity/lib/components/channelinfocontainer/index.coffee
+++ client/activity/lib/components/channelinfocontainer/index.coffee
@@ -13,6 +13,7 @@
 module.exports = class ChannelInfoContainer extends React.Component

   @defaultProps =
+    afterInviteOthers: kd.noop
     thread: immutable.Map()

@@ -49,6 +50,8 @@
     kd.utils.stopDOMEvent event

     ChatInputFlux.actions.value.setValue @props.thread.get('channelId'), '/invite @'
+
+    @props.afterInviteOthers()

   renderChannelName: ->


PendingInvitationView: Implemented search.

--- client/admin/lib/views/invitations/pendinginvitationsview.coffee
+++ client/admin/lib/views/invitations/pendinginvitationsview.coffee
@@ -9,9 +9,10 @@

   constructor: (options = {}, data) ->

-    options.listViewItemClass     = InvitedItemView
-    options.listViewItemOptions or= statusType: 'pending'
-    options.statusType          or= 'pending'
+    options.listViewItemClass        = InvitedItemView
+    options.searchInputPlaceholder   = 'Find by email or first name'
+    options.listViewItemOptions    or= statusType: 'pending'
+    options.statusType             or= 'pending'

     super options, data

@@ -22,11 +23,16 @@

     @isFetching    = yes
     { statusType } = @getOptions()
+    query          = @searchInput.getValue()
+    options        = { @skip }
+    method         = 'some'
+    selector       = status: statusType

-    selector = status: statusType
-    options  = { @skip }
+    if query
+      method = 'search'
+      selector.query = query

-    remote.api.JInvitation.some selector, options, (err, invitations) =>
+    remote.api.JInvitation[method] selector, options, (err, invitations) =>

       if err
         @listController.lazyLoader.hide()


Don't display invite to group on pending mem card

--- client/angular/components/membership/card/membership_card.coffee
+++ client/angular/components/membership/card/membership_card.coffee
@@ -49,7 +49,7 @@
       !$scope.loader.loading

     $scope.canAddMembers = ->
-      AbilityService.canAddMembers($scope.group)
+      AbilityService.canAddMembers($scope.group) && !$scope.pending

     $scope.memberships = ->
       if $scope.vars.fragment


Rewrite data/config mapping, handle single nodes

--- source/coffee/index.coffee
+++ source/coffee/index.coffee
@@ -5,8 +5,24 @@

 $ ->
   IndexModel = ->
-    @wsTemp = ko.observableArray([])
-    @wsItems = ko.observableArray([])
+    @getData = (url, cb) ->
+      $.getJSON url, (data) ->
+        cb data if cb?
+
+    @mapData = (data, pin, map={}, wrap=true) ->
+      data = [data] if wrap?
+      ko.mapping.fromJS data, map, pin
+
+    @getMappedData = (url, pin, map={}, wrap=true) =>
+      @getData url, (data) => @mapData(data, pin, map, wrap)
+
+    @config = {}
+    @getData "/api/config/timeout", (data) =>
+      @config.timeout = data
+
+    @wsTemp = ko.observableArray()
+    @wsItems = ko.computed =>
+      @wsTemp()?[0]?.children ? []

     # TODO: Map from data source
     @siteNav = ko.observableArray [
@@ -26,6 +42,9 @@
           ko.utils.unwrapObservable data.id
         create: (options) ->
           createNode options
+      node:
+        key: (data) ->
+          ko.utils.unwrapObservable data.id

     createNode = (options) =>
       @node = options.data


Refresh media folder view after each media deletion.

--- MediaAdminBundle/Resources/public/coffee/GalleryView.coffee
+++ MediaAdminBundle/Resources/public/coffee/GalleryView.coffee
@@ -67,7 +67,7 @@
       url: @options.media.get("links")._self_delete
       method: 'Delete'
       success: (response) ->
-        target.parents(".superbox-list").remove()
+        Backbone.history.loadUrl(Backbone.history.getFragment())

   mediaSelect : (event) ->
     event.preventDefault()


Color Mapping cHANGE

Color Mapping cHANGE

--- site/app.coffee
+++ site/app.coffee
@@ -211,8 +211,27 @@
     return 9
   return 10

-level2Color = (level) ->
-  return Math.max(0, Math.min(10, level)) / 10
+level2Color = (l) ->
+  if l == 1
+    return '#ff99ff'
+  if l == 2
+    return '#ff66ff'
+  if l == 3
+    return '#ff66cc'
+  if l == 4
+    return '#ff00ff'
+  if l == 5
+    return '#cc00cc'
+  if l == 6
+    return '#cc0099'
+  if l == 7
+    return '#cc3399'
+  if l == 8
+    return '#993399'
+  if l == 9
+    return '#800080'
+  if l == 10
+    return '#660066'

 Date.prototype.getWeek = () ->
   stdDate = new Date(2016, 0, 3)
@@ -451,8 +470,7 @@
             if row.epiweek == epiweek2
               ili = row.value
               v = colorData[row.location]
-              c = ('0' + Math.round(0x3f + v * 0xc0).toString(16)).slice(-2)
-              @colors[row.location] = '#' + c + '4040'
+              @colors[row.location] = v
               @mapData[row.location] = row
           @renderMap()
         handler = getEpidataHander(callback2)


Readded all transportation methods for socket.io.

--- qest.coffee
+++ qest.coffee
@@ -49,15 +49,6 @@
     io.enable('browser client etag');          # apply etag caching logic based on version number
     io.enable('browser client gzip');          # gzip the file
     io.set('log level', 1)
-
-    io.set('transports', [
-      'htmlfile'
-    , 'xhr-polling'
-    , 'jsonp-polling'
-    ])
-
-  io.configure 'development', ->
-    io.set('transports', ['websocket'])

   # Helpers
   helpersPath = __dirname + "/app/helpers/"


Replace stdio with stdin/stdout pair for readline

On Node.js v0.3.6, the readline interface expects (in, out, complete).
This change makes the coffee-script repl conform to that expectation.

--- repl.coffee
+++ repl.coffee
@@ -12,11 +12,12 @@
 # REPL Setup

 # Start by opening up **stdio**.
-stdio = process.openStdin()
+stdin = process.openStdin()
+stdout = process.stdout

 # Log an error.
 error = (err) ->
-  stdio.write (err.stack or err.toString()) + '\n\n'
+  stdout.write (err.stack or err.toString()) + '\n\n'

 # The current backlog of multi-line code.
 backlog = ''
@@ -76,9 +77,8 @@
 process.on 'uncaughtException', error

 # Create the REPL by listening to **stdin**.
-repl = readline.createInterface stdio, autocomplete
+repl = readline.createInterface stdin, stdout, autocomplete
 repl.setPrompt 'coffee> '
-stdio.on 'data',   (buffer) -> repl.write buffer
-repl.on  'close',  -> stdio.destroy()
+repl.on  'close',  -> stdin.destroy()
 repl.on  'line',   run
 repl.prompt()


Fix tests to properly handle windows \r\n line endings

--- test/xml2js.test.coffee
+++ test/xml2js.test.coffee
@@ -4,6 +4,7 @@
 util = require 'util'
 assert = require 'assert'
 path = require 'path'
+os = require 'os'

 fileName = path.join __dirname, '/fixtures/sample.xml'

@@ -16,7 +17,8 @@
       checks r
       test.finish()
     if not xmlString
-      fs.readFile fileName, (err, data) ->
+      fs.readFile fileName, 'utf8', (err, data) ->
+        data = data.split(os.EOL).join('\n')
         x2js.parseString data
     else
       x2js.parseString xmlString


Stopgap measure for incorrectly formatted session results

--- server/queues/scoring.coffee
+++ server/queues/scoring.coffee
@@ -363,7 +363,6 @@
         "sessionID": session._id
         "submitDate": session.submitDate
         "team": session.team ? "No team"
-        "code": session.submittedCode
         "transpiledCode": session.transpiledCode
         "teamSpells": session.teamSpells ? {}
         "levelID": session.levelID
@@ -403,35 +402,37 @@

 module.exports.processTaskResult = (req, res) ->
   originalSessionID = req.body?.originalSessionID
-  async.waterfall [
-    verifyClientResponse.bind(@,req.body)
-    fetchTaskLog.bind(@)
-    checkTaskLog.bind(@)
-    deleteQueueMessage.bind(@)
-    fetchLevelSession.bind(@)
-    checkSubmissionDate.bind(@)
-    logTaskComputation.bind(@)
-    updateSessions.bind(@)
-    indexNewScoreArray.bind(@)
-    addMatchToSessions.bind(@)
-    updateUserSimulationCounts.bind(@, req.user._id)
-    determineIfSessionShouldContinueAndUpdateLog.bind(@)
-    findNearestBetterSessionID.bind(@)
-    addNewSessionsToQueue.bind(@)
-  ], (err, results) ->
-    if err is "shouldn't continue"
-      markSessionAsDoneRanking originalSessionID, (err) ->
-        if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
-        sendResponseObject req, res, {"message":"The scores were updated successfully, person lost so no more games are being inserted!"}
-    else if err is "no session was found"
-      markSessionAsDoneRanking originalSessionID, (err) ->
-        if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
-        sendResponseObject req, res, {"message":"There were no more games to rank (game is at top)!"}
-    else if err?
-      errors.serverError res, "There was an error:#{err}"
-    else
-      sendResponseObject req, res, {"message":"The scores were updated successfully and more games were sent to the queue!"}
-
+  try
+    async.waterfall [
+      verifyClientResponse.bind(@,req.body)
+      fetchTaskLog.bind(@)
+      checkTaskLog.bind(@)
+      deleteQueueMessage.bind(@)
+      fetchLevelSession.bind(@)
+      checkSubmissionDate.bind(@)
+      logTaskComputation.bind(@)
+      updateSessions.bind(@)
+      indexNewScoreArray.bind(@)
+      addMatchToSessions.bind(@)
+      updateUserSimulationCounts.bind(@, req.user._id)
+      determineIfSessionShouldContinueAndUpdateLog.bind(@)
+      findNearestBetterSessionID.bind(@)
+      addNewSessionsToQueue.bind(@)
+    ], (err, results) ->
+      if err is "shouldn't continue"
+        markSessionAsDoneRanking originalSessionID, (err) ->
+          if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
+          sendResponseObject req, res, {"message":"The scores were updated successfully, person lost so no more games are being inserted!"}
+      else if err is "no session was found"
+        markSessionAsDoneRanking originalSessionID, (err) ->
+          if err? then return sendResponseObject req, res, {"error":"There was an error marking the session as done ranking"}
+          sendResponseObject req, res, {"message":"There were no more games to rank (game is at top)!"}
+      else if err?
+        errors.serverError res, "There was an error:#{err}"
+      else
+        sendResponseObject req, res, {"message":"The scores were updated successfully and more games were sent to the queue!"}
+  catch e
+    errors.serverError res, "There was an error processing the task result!"
 verifyClientResponse = (responseObject, callback) ->
   #TODO: better verification
   if typeof responseObject isnt "object" or responseObject?.originalSessionID?.length isnt 24
@@ -716,7 +717,10 @@

 putRankingFromMetricsIntoScoreObject = (taskObject,scoreObject) ->
   scoreObject = _.indexBy scoreObject, 'id'
-  scoreObject[session.sessionID].gameRanking = session.metrics.rank for session in taskObject.sessions
+  try
+    scoreObject[session.sessionID].gameRanking = session.metrics.rank for session in taskObject.sessions
+  catch e
+    console.log "There was an error setting gameRanking for session #{JSON.stringify session}"
   return scoreObject

 retrieveOldSessionData = (sessionID, callback) ->


Switch account link, fixed double chart bug

--- src/javascript/main.coffee
+++ src/javascript/main.coffee
@@ -359,23 +359,12 @@

   render: ->
     if @state.ssoState == 'login'
-      ssoLoginImage = 'https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45'
-      ssoParams =
-        response_type: 'token'
-        redirect_uri: SSO_CALLBACK_URL
-        client_id: SSO_CLIENT_ID
-        #scope: 'publicData'
-        scope: 'publicData characterStatisticsRead'
-        state: 'testState'
-
-      ssoUrl = "#{SSO_HOST}/oauth/authorize/?#{qs.stringify ssoParams}"
-      return dom.a {href: ssoUrl}, dom.img {src: ssoLoginImage}
+      return React.createElement(SSOLoginButton)
     else if @state.ssoState == 'loading'
       return dom.div null, "Verifying SSO..."
+    else if not @state.stats
+      return dom.div null, "Loading Your Stats..."
     else
-
-      if not @state.stats
-        return dom.div null, "Loading Your Stats..."

       facts = new CharacterFacts(@state.stats).getFacts()

@@ -439,6 +428,8 @@
       rawStatsList = React.createElement(RawStatsList, {stats: @state.stats})

       dom.div {className: 'container translucent'},
+
+        React.createElement(SSOLoginButton, {linkText: 'Switch Character/Account'})

         title

@@ -515,6 +506,27 @@
         # Misc

         #rawStatsList
+)
+
+SSOLoginButton = React.createClass(
+  displayName: 'SSOLoginButton'
+  render: ->
+    ssoLoginImage = 'https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45'
+    ssoParams =
+      response_type: 'token'
+      redirect_uri: SSO_CALLBACK_URL
+      client_id: SSO_CLIENT_ID
+      scope: 'publicData characterStatisticsRead'
+      state: 'testState'
+
+    ssoUrl = "#{SSO_HOST}/oauth/authorize/?#{qs.stringify ssoParams}"
+
+    if @props.linkText
+      content = @props.linkText
+    else
+      content = dom.img {src: ssoLoginImage}
+
+    return dom.a {href: ssoUrl}, content
 )

 Title = React.createClass(
@@ -1367,7 +1379,9 @@
               .tickFormat (d) ->
                 STYLES[d].label

-    svg = d3.select(el).append('svg')
+    svg = d3.select(el).selectAll('svg').data [@props.data]
+
+    svg.enter().append('svg')
               .attr 'width', @props.width + @props.margin.left + @props.margin.right
               .attr 'height', @props.height + @props.margin.top + @props.margin.top
             .append 'g'
@@ -1433,7 +1447,9 @@

     data = pie(@props.data)

-    svg = d3.select(el).append('svg')
+    svg = d3.select(el).selectAll('svg').data [data]
+
+    svg.enter().append('svg')
             .attr 'width', @props.width
             .attr 'height', @props.height

@@ -1472,6 +1488,8 @@
     if @props.data.length == 0
       return

+    console.log 'ship hp chart'
+
     el = @getDOMNode()

     radius = 90
@@ -1494,7 +1512,9 @@

     data = @props.data

-    svg = d3.select(el).append('svg')
+    svg = d3.select(el).selectAll('svg').data [data]
+
+    svg.enter().append('svg')
             .attr 'width', @props.width
             .attr 'height', @props.height


Defaulting state to null

Defaulting state to null

--- src/javascript/main.coffee
+++ src/javascript/main.coffee
@@ -301,9 +301,9 @@
       token: null
       stats: null
       character:
-        name: 'Bellatroix'
-        id: 1412571394
-      year: 2014
+        name: null
+        id: null
+      year: null
     }
   componentDidMount: ->
     hash = window.location.hash.substring(1)


Using more than 1 container is bad.  Fixed weird width issue by properly using nested rows

--- src/javascript/main.coffee
+++ src/javascript/main.coffee
@@ -403,7 +403,7 @@
           dom.div {className: 'col-md-6'}, repsReceivedPanel

         dom.div {className: 'row'},
-          dom.div {className: 'col-md-12'}, selfRepPanel
+          dom.div {className: 'col-md-6'}, selfRepPanel

         # PVE Stats

@@ -449,7 +449,7 @@
 CharacterInfoPanel = React.createClass(
   displayName: 'CharacterInfoPanel'
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: 'row'},
       dom.div {className: 'col-md-4'},
         React.createElement(CharacterAvatar, {id: @props.character.id})
       dom.div {className: 'col-md-8'},
@@ -492,7 +492,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: ''},
       dom.h3 null, 'Stargate / Wormhole Jumps'
       React.createElement(PieDataPanel,{data: @chartData(), chartType: 'pie'})
 )
@@ -520,7 +520,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: ''},
       dom.h3 null, 'Warp Distance Traveled (AU)'
       React.createElement(PieDataPanel,{data: @chartData(), chartType: 'pie'})
 )
@@ -667,7 +667,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: ''},
       dom.h3 null,
         'Damage Dealt'
         ' '
@@ -714,7 +714,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: ''},
       dom.h3 null,
         'Damage Taken'
         ' '
@@ -746,7 +746,7 @@
       dom.h4 {className: 'pull-right'},
         dom.em null,"You have dealt enough damage to kill #{numShips} "
           dom.a {href: ship.fit}, ship.pluralName
-      dom.div {className: 'container'}, stamps
+      dom.div {className: 'row'}, stamps

 )

@@ -827,7 +827,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: 'row'},
       dom.h3 null, 'Local Reps'
         React.createElement(PieDataPanel,{data: @chartData(), chartType: 'shipHp'})
 )
@@ -851,7 +851,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: 'row'},
       dom.h3 null, 'Remote Reps Given'
         React.createElement(PieDataPanel,{data: @chartData(), chartType: 'shipHp'})
 )
@@ -875,7 +875,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: 'row'},
       dom.h3 null, 'Reps Received'
         React.createElement(PieDataPanel,{data: @chartData(), chartType: 'shipHp'})
 )
@@ -899,7 +899,7 @@
 MiscModulePanel = React.createClass(
   displayName: 'MiscModulePanel'
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: ''},
       dom.h3 null, 'Modules'
       dom.ul null,
         dom.li null, "Lit #{@props.stats.moduleActivationsCyno} cynos"
@@ -954,7 +954,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: 'row'},
       dom.h3 null, 'Manufacturing Jobs'
         React.createElement(PieDataPanel,{data: @chartData()})
 )
@@ -1066,7 +1066,7 @@
     ]
     return data
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: 'row'},
       dom.h3 null, 'Mining'
         React.createElement(PieDataPanel,{data: @chartData()})
 )
@@ -1130,7 +1130,7 @@
         value: @props.stats["social#{str}ContactHorrible"]
       }
     ]
-    dom.div {className: 'container'},
+    dom.div {className: 'row'},
       dom.h3 null,
         title
       React.createElement(PieDataPanel, {data: data})
@@ -1140,7 +1140,7 @@
 SocialMiscPanel = React.createClass(
   displayName: 'SocialMiscPanel'
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: ''},
       dom.ul null,
         dom.li null, "#{Intl.NumberFormat().format(@props.stats.socialChatTotalMessageLength)} characters written in chat"
         dom.li null, "#{@props.stats.socialMailsSent} mails sent"
@@ -1153,7 +1153,7 @@
 MiscStats = React.createClass(
   displayName: 'MiscStats'
   render: ->
-    dom.div {className: 'container'},
+    dom.div {className: ''},
       dom.ul null,
         dom.li null, "#{@props.stats.genericConeScans} Directional Scans"
         dom.li null, "#{@props.stats.genericRequestScans} Probe Scans"
@@ -1193,10 +1193,10 @@
         chartElementType = ShipHPChart
         sortFn = (a, b) -> return HP_BAR_ORDER.indexOf(a.key) - HP_BAR_ORDER.indexOf(b.key)
     data = @deriveData(sortFn)
-    dom.div null,
-      dom.div {className: 'col-md-2'},
+    dom.div {className: 'row'},
+      dom.div {className: 'col-sm-4'},
         React.createElement(chartElementType, {data: data})
-      dom.div {className: 'col-md-3'},
+      dom.div {className: 'col-sm-6'},
         React.createElement(PieDataTable,{data: data, total: @total})
 )


cli: Also output stack if present

cli: Also output stack if present

--- src/cli.coffee
+++ src/cli.coffee
@@ -69,6 +69,7 @@
   runJob options, (err, results, details) ->
     if err
       console.error err
+      console.error err.stack if err.stack
       process.exit 1

     out = JSON.stringify results, null, 2


Move logError to the very top

Move logError to the very top

--- src/cli.coffee
+++ src/cli.coffee
@@ -9,6 +9,10 @@
 fs = require 'fs'

 if argv.env then config.env argv.env
+
+logError = (err) ->
+  console.error chalk.red(err)
+  process.exit(1)

 commands =
   help: (argv) ->
@@ -119,7 +123,3 @@
   command = 'help'

 commands[command].call this, argv
-
-logError = (err) ->
-  console.error chalk.red(err)
-  process.exit(1)


cleanup

cleanup

--- resources/assets/coffee/react/_components/beatmapset-panel.coffee
+++ resources/assets/coffee/react/_components/beatmapset-panel.coffee
@@ -16,7 +16,7 @@
 #    along with osu!web.  If not, see <http://www.gnu.org/licenses/>.
 ###

-{div,a,i,img,span} = ReactDOMFactories
+{div,a,i,span} = ReactDOMFactories
 el = React.createElement

 class @BeatmapsetPanel extends React.PureComponent
@@ -45,7 +45,7 @@

   hideImage: (e) ->
     # hides img elements that have errored (hides native browser broken-image icons)
-    e.currentTarget.style.display = 'none';
+    e.currentTarget.style.display = 'none'

   render: =>


Reluctantly use soft-tabs for indenting Lisp

Alien technology; not my fault.

--- src/configs/user-config.cson
+++ src/configs/user-config.cson
@@ -106,6 +106,10 @@
 ".atom-mocha":
   editor:
     fontSize: 13
+".lisp.source":
+  editor:
+    softTabs: true
+    tabLength: 2
 ".rust.source":
   editor:
     softTabs: false


testing...

testing...

--- test/christacheio-spec.coffee
+++ test/christacheio-spec.coffee
@@ -94,3 +94,24 @@

     it 'should replace the mustached area', ->
       expect(@result).to.deep.equal 'walnut'
+
+  describe 'when called with a christacheio as the entire string and an object value', ->
+    beforeEach ->
+      @result = christacheio '{{nut}}', nut: favorite: 'pecan'
+
+    it 'should return an object instead of a string', ->
+      expect(@result).to.deep.equal favorite: 'pecan'
+
+  describe 'when called with a string containing an object value and no transformation', ->
+    beforeEach ->
+      @result = christacheio '{{nut}}...?', nut: favorite: 'pecan'
+
+    it 'should replace the mustached area with [object Object]', ->
+      expect(@result).to.deep.equal '[object Object]...?'
+
+  describe 'when called with a string containing an object key and a transformation', ->
+    beforeEach ->
+      @result = christacheio '{{nut}}...?', {nut: favorite: 'pecan'}, {transformation: JSON.stringify}
+
+    it 'should replace the mustached area with JSON stringified version of the object', ->
+      expect(@result).to.deep.equal '{"favorite":"pecan"}...?'


visit: visit is the same as {visit}

visit: visit is the same as {visit}

Coffee provides a shortcut for that.

See http://coffeescript.org/#try:visit%20%3D%20-%3E%0A%40Turbolinks%20%3D%20%7Bvisit%7D

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -160,4 +160,4 @@
     handleClick event

 # Call Turbolinks.visit(url) from client code
-@Turbolinks = visit: visit
+@Turbolinks = {visit}


Handle error

Handle error

--- spec/RemoteRuntimes.coffee
+++ spec/RemoteRuntimes.coffee
@@ -206,7 +206,8 @@
         noflo.graph.loadFBP echoNoflo, (err, graph) ->
           graph = err if not graph # NoFlo <0.6 compat
           graph.setProperties { id: 'echoNoflo', main: true }
-          c.setGraph graph, ->
+          c.setGraph graph, (err) ->
+            return done err if err
             c.runtime.start()
     it 'should have exported inport and outport', (done) ->
       checkPorts = () ->


Test the ascii conversion

Test the ascii conversion

--- spec/ascii-art-spec.coffee
+++ spec/ascii-art-spec.coffee
@@ -16,7 +16,19 @@
       atom.workspace.open()

   it "converts", ->
+    editor = atom.workspace.getActiveTextEditor()
+    editor.insertText("yo")
+    editor.selectAll()
+    changeHandler = jasmine.createSpy('changeHandler')
+    editor.onDidChange(changeHandler)
+
     atom.commands.dispatch workspaceElement, 'ascii-art:convert'

     waitsForPromise ->
       activationPromise
+
+    waitsFor ->
+      changeHandler.callCount > 0
+
+    runs ->
+      expect(editor.getText()).toEqual "\n                 \n                 \n __  __    ___   \n/\\ \\/\\ \\  / __`\\ \n\\ \\ \\_\\ \\/\\ \\L\\ \\\n \\/`____ \\ \\____/\n  `/___/> \\/___/ \n     /\\___/      \n     \\/__/       \n"


Reword spec

Reword spec

--- spec/find-view-spec.coffee
+++ spec/find-view-spec.coffee
@@ -564,7 +564,7 @@
         findView.replaceEditor.trigger 'core:confirm'
         expect(editor.getText()).toMatch(/\t\n/)

-      it "carriage returns", ->
+      it "inserts carriage returns", ->
         textWithCarriageReturns = editor.getText().replace(/\n/g, "\r")
         editor.setText(textWithCarriageReturns)


Simulate typing in a slightly more realistic way.

This allows us to type directly into input elements, too. That won't do anything by default, but it'll be useful for asserting that a particular TextField didn't do anything as a result of typing into an input.

--- spec/helpers/typing.coffee
+++ spec/helpers/typing.coffee
@@ -1,23 +1,27 @@
 FakeEvent = require './fake_event'
+TextField = require '../../lib/text_field'

 class Type
   constructor: (keys...) ->
     @keys = keys

-  into: (@field) ->
+  into: (element) ->
+    if element instanceof TextField
+      element = element.element
+    @element = element
     @perform()
     return this

   perform: ->
     for event in FakeEvent.eventsForKeys(@keys...)
       event.type = 'keydown'
-      @field.keyDown event
+      @element.trigger event.type, event
       if not event.isDefaultPrevented()
         event.type = 'keypress'
         if event.charCode
-          @field.keyPress event
+          @element.trigger event.type, event
       event.type = 'keyup'
-      @field.keyUp event
+      @element.trigger event.type, event

 type = (keys...) ->
   new Type(keys...)


new backgroundColor and image properties for Canvas object

--- framer/Canvas.coffee
+++ framer/Canvas.coffee
@@ -7,6 +7,18 @@
 	@define "height", get: -> window.innerHeight
 	@define "size", get: -> {width:@width, height:@height}
 	@define "frame", get: -> {x:0, y:0, width:@width, height:@height}
+
+	@define "backgroundColor",
+		importable: false
+		exportable: false
+		get: -> Framer.Device.background.backgroundColor
+		set: (value) -> Framer.Device.background.backgroundColor = value
+
+	@define "image",
+		importable: false
+		exportable: false
+		get: -> Framer.Device.background.image
+		set: (value) -> Framer.Device.background.image = value

 	addListener: (eventName, listener) =>
 		if eventName is "resize"


fix(spec): fix nylas api specs

fix(spec): fix nylas api specs

--- spec/nylas-api-spec.coffee
+++ spec/nylas-api-spec.coffee
@@ -51,14 +51,14 @@
       spyOn(Actions, 'postNotification')
       NylasAPI._handleAuthenticationFailure('/threads/1234', 'token')
       expect(Actions.postNotification).toHaveBeenCalled()
-      expect(Actions.postNotification.mostRecentCall.args[0].message.trim()).toEqual("Nylas N1 can no longer authenticate with your mail provider. You will not be able to send or receive mail. Please click here to reconnect your account.")
+      expect(Actions.postNotification.mostRecentCall.args[0].message.trim()).toEqual("Action failed: There was an error syncing with your mail provider. You may not be able to send or receive mail.")

     it "should include the email address if possible", ->
       spyOn(AccountStore, 'tokenForAccountId').andReturn('token')
       spyOn(Actions, 'postNotification')
       NylasAPI._handleAuthenticationFailure('/threads/1234', 'token')
       expect(Actions.postNotification).toHaveBeenCalled()
-      expect(Actions.postNotification.mostRecentCall.args[0].message.trim()).toEqual("Nylas N1 can no longer authenticate with #{AccountStore.accounts()[0].emailAddress}. You will not be able to send or receive mail. Please click here to reconnect your account.")
+      expect(Actions.postNotification.mostRecentCall.args[0].message.trim()).toEqual("Action failed: There was an error syncing with #{AccountStore.accounts()[0].emailAddress}. You may not be able to send or receive mail.")

   describe "handleModelResponse", ->
     beforeEach ->


Update scroll positions in specs

Update scroll positions in specs

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -394,13 +394,13 @@
         waitsForPromise -> atom.workspace.open(path.join(rootDirPath, 'file-20.txt'))
         runs ->
           atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file')
-          expect(treeView.scrollTop()).toEqual 440
+          expect(treeView.scrollTop()).toEqual 425

         # Open file in the middle, should be centered in scroll
         waitsForPromise -> atom.workspace.open(path.join(rootDirPath, 'file-10.txt'))
         runs ->
           atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file')
-          expect(treeView.scrollTop()).toEqual 220
+          expect(treeView.scrollTop()).toEqual 213

         # Open file at top
         waitsForPromise -> atom.workspace.open(path.join(rootDirPath, 'file-1.txt'))


Added brain.remove

Added brain.remove

An afterthought.

--- src/brain.coffee
+++ src/brain.coffee
@@ -34,6 +34,13 @@
   # Returns the value.
   get: (key) ->
     @data._private[key] ? null
+
+  # Public: Remove value by key from the private namespace in @data
+  # if it exists
+  #
+  # Returns the instance for chaining.
+  remove: (key) ->
+    delete @data._private[key] if @data._private[key]?

   # Public: Emits the 'save' event so that 'brain' scripts can handle
   # persisting.


conditional on container type

conditional on container type

--- src/chuck.coffee
+++ src/chuck.coffee
@@ -7,7 +7,7 @@
 operators = /\+|-|\*|\//

 resolveContainer = (type) ->
-  if type.container?
+  if type?.container?
     contains = (resolveContainer t for t in [].concat type.contains).join ', '
     if type.container is '[]'
       "#{contains}[]"


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -41,6 +41,7 @@
     parsedColor = null

     if arguments.length == 2
+      if arguments[0].length == 3
       parsedColor = [parseInt(arguments[0][0]), parseInt(arguments[0][1]), parseInt(arguments[0][2]), parseFloat(arguments[1])]
     else if arguments.length > 2
       alpha = if arguments[3]? then arguments[3] else 1


:memo: Link to config method

:memo: Link to config method

--- src/color.coffee
+++ src/color.coffee
@@ -1,7 +1,7 @@
 _ = require 'underscore-plus'
 ParsedColor = require 'color'

-# Essential: A simple Color class returned from `atom.config.get` when the value
+# Essential: A simple color class returned from {Config::get} when the value
 # at the key path is of type 'color'.
 module.exports =
 class Color


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -610,13 +610,19 @@
     ###*
     Returns a copy of the calling color lightened by `amount` (Lightness of the color ranges from 0 to 1).

-

+

     green = Color(0, 255, 0)

     lightGreen = green.lighten(0.2)

-    lightGreen.toString()
-    # => 'rgba(102, 255, 102, 1)'
+    # to see what they look like
+    for color, index in ['green', 'lightGreen']
+      canvas.drawRect
+        color: eval(color)
+        x: 20 + (60 * index)
+        y: 20 + (60 * index)
+        width: 60
+        height: 60

     @name lighten


added a few code comments, need to add more

--- src/fussy.coffee
+++ src/fussy.coffee
@@ -1,9 +1,13 @@
+# STANDARD NODE LIBRARY MODULES
 {inspect} = require 'util'
 fs        = require 'fs'
+
+# THIRD PARTIES MODULES
 deck      = require 'deck'

-pretty = (obj) -> "#{inspect obj, no, 20, yes}"
-wait = (t) -> (f) -> setTimeout f, t
+# HELPERS
+pretty = (obj) -> "#{inspect obj, no, 20, yes}" # prettify an object to colorful string
+wait = (t) -> (f) -> setTimeout f, t            # just an alias to setTimeout

 #################################
@@ -28,18 +32,31 @@
   grams

+###
+
+
+###
 class Database

   constructor: (@_={}) ->
-    @ngramSize = 3
+    @ngramSize = 3 # we will generate tuples of 1, 2 and 3 words
     @size = 0

-  learn: (tagged, b) =>
-    if b?
+  ###
+  usage:
+     learn { "sentence" : [ tags, .. ] }
+
+     learn "sentence, [ tags, .. ]
+  ###
+  learn: (tagged, value) =>
+
+    # learn "sentence, [ tags, .. ]
+    if value?
       _tagged = {}
-      _tagged["#{tagged}"] = b
+      _tagged["#{tagged}"] = value
       tagged = _tagged

+    # learn { "sentence" : [ tags, .. ] }
     for txt, keywords of tagged
       for n, ngram of ngramize txt, @ngramSize
         #console.log "n: #{n}, ngram: #{ngram}"
@@ -73,7 +90,11 @@
             keywords[k] += count
       keywords

-  # delete connections of weight inferior or equal to a threshold
+  ###
+
+  we need to delete connections of weight inferior or equal to a threshold
+
+  ###
   prune: (threshold, onComplete) =>
     pruned =
       keywords: 0
@@ -153,4 +174,4 @@
       score: i[1]

 exports.Database = Database
-exports.Profile = Profile+exports.Profile = Profile


Remove unused validatOnly prop

Remove unused validatOnly prop

--- src/course/registration.cjsx
+++ src/course/registration.cjsx
@@ -10,16 +10,15 @@

   propTypes:
     collectionUUID: React.PropTypes.string.isRequired
-    validateOnly: React.PropTypes.bool

   mixins: [UserStatus]

   render: ->
     course = @getUser().getCourse(@props.collectionUUID)
-    body = if course and not @props.validateOnly
+    body = if course and course.isRegistered()
       <ModifyCourseRegistration {...@props} course={course} />
     else
-      <NewCourseRegistration {...@props} validateOnly={@props.validateOnly} />
+      <NewCourseRegistration {...@props} />

     <div className="row">
       {body}


fix(undo-redo): Remove initial state check

--- internal_packages/undo-redo/lib/undo-redo-component.cjsx
+++ internal_packages/undo-redo/lib/undo-redo-component.cjsx
@@ -16,8 +16,10 @@

   constructor: (@props) ->
     @_timeout = null
-    @state = @_getStateFromStores()
-    @_ensureTimeout(@state)
+
+    # Note: we do not set from initial state, because we don't want
+    # the last item on the stack to appear, just the next one.
+    @state = {}

   _clearTimeout: =>
     clearTimeout(@_timeout)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -118,7 +118,7 @@
   ]

   4.circularPoints (p, i) ->
-
+    e
     ok p.equal(points[i]), "#{p} == #{points[i]}"

 module undefined


make user-merge modal large

make user-merge modal large

--- app/assets/javascripts/app/controllers/layout_ref.js.coffee
+++ app/assets/javascripts/app/controllers/layout_ref.js.coffee
@@ -1580,6 +1580,7 @@
     @html App.view('layout_ref/merge_customer_view')

     new App.ControllerModal
+      large: true
       head: "Merge #{@mergeSource.firstname} #{@mergeSource.lastname}"
       content: App.view('layout_ref/merge_customer')()
       button: 'Merge'


Remove old popovers if search result is empty.

--- app/assets/javascripts/app/controllers/navigation.js.coffee
+++ app/assets/javascripts/app/controllers/navigation.js.coffee
@@ -49,6 +49,9 @@
     if _.isEmpty( result )
       @el.find('#global-search').parents('li').removeClass('open')
       el.html( '' )
+
+      # remove old popovers
+      $('.popover').remove()
       return

     # show result list


don't auto load session info on document.ready so url can be overridden

--- app/assets/javascripts/augmentations/session_info.js.coffee
+++ app/assets/javascripts/augmentations/session_info.js.coffee
@@ -1,12 +1,13 @@
 class Augmentations.SessionInfo
   @sessionInfoUrl = '/session/info.json'
+  constructor: (@url=SessionInfo.sessionInfoUrl)->
   loading: =>
     @spinner = $('<div/>').appendTo('#hd_masthead_sessionmenu').spin({color:'#fff', lines: 12})
   doneLoading: =>
     @spinner.remove()
   load: =>
     @loading()
-    $.getJSON SessionInfo.sessionInfoUrl
+    $.getJSON @url
   display: =>
     @load().success (data)=>
       @session = data
@@ -16,6 +17,3 @@
   triggerEvent: =>
     event = jQuery.Event('sessionLoad')
     $(document).trigger(event, [@session])
-$(->
-  (new Augmentations.SessionInfo).display()
-)


Remove errant debugger

Remove errant debugger

--- app/assets/javascripts/components/main-controller.js.coffee
+++ app/assets/javascripts/components/main-controller.js.coffee
@@ -5,7 +5,6 @@
   render: ->
     div className: "main-container",
       div className: "main--league-list-container",
-        debugger
         LeagueList
           leagues: @props.leagues


Automatically put name in received private messages, fixes #61

--- src/irc.coffee
+++ src/irc.coffee
@@ -183,6 +183,10 @@
     bot.addListener 'pm', (nick, message) ->
       console.log('Got private message from %s: %s', nick, message)

+      nameLength = options.nick.length
+      if message.slice(0, nameLength).toLowerCase() != options.nick.toLowerCase()
+        message = "#{options.nick} #{message}"
+
       self.receive new TextMessage({reply_to: nick, name: nick}, message)

     bot.addListener 'join', (channel, who) ->


Don't start of in the middle of the ocean

--- web2/src/stores/map.coffee
+++ web2/src/stores/map.coffee
@@ -8,7 +8,7 @@
     constructor: (flux) ->
         super flux,
             initialState:
-                center: [0, 0]
+                center: [52.518769, 13.404027] # somewhere in Berlin
                 zoom: 13
                 selectedTypes: Immutable.Set.fromKeys geocaches.types
                 filterUsers: Immutable.Set()


this is broken and useless

this is broken and useless

--- scripts/lol_subreddit.coffee
+++ scripts/lol_subreddit.coffee
@@ -1,60 +0,0 @@
-# Description:
-#   /r/leagueoflegends
-#
-# Dependencies:
-#   "nodepie": "0.5.0"
-#
-# Configuration:
-#   None
-#
-# Commands:
-#   hubot /r/lol top <N> - get the top N items on /r/lol
-#   /r/lol.top - refer to the top item on /r/lol
-#   /r/lol[i] - refer to the ith item on /r/lol
-#
-# Author:
-#   skimbrel - hackernews
-#   ivey
-
-NodePie = require("nodepie")
-
-lolFeedUrl = "http://www.reddit.com/r/leagueoflegends.rss"
-
-module.exports = (robot) ->
-  robot.respond /\/r\/lol top (\d+)?/i, (msg) ->
-    msg.http(lolFeedUrl).get() (err, res, body) ->
-      if res.statusCode is not 200
-        msg.send "Something's gone awry"
-      else
-        feed = new NodePie(body)
-        try
-          feed.init()
-          count = msg.match[1] || 5
-          items = feed.getItems(0, count)
-          msg.send item.getTitle() + ": " + item.getPermalink() for item in items
-        catch e
-          console.log(e)
-          msg.send "Something's gone awry"
-
-  robot.hear /\/r\/lol(\.top|\[\d+\])/i, (msg) ->
-     msg.http(lolFeedUrl).get() (err, res, body) ->
-       if res.statusCode is not 200
-         msg.send "Something's gone awry"
-       else
-         feed = new NodePie(body)
-         try
-           feed.init()
-         catch e
-           console.log(e)
-           msg.send "Something's gone awry"
-         element = msg.match[1]
-         if element == "/r/lol.top"
-           idx = 0
-         else
-           idx = (Number) msg.match[0].replace(/[^0-9]/g, '')
-         try
-           item = feed.getItems()[idx]
-           msg.send item.getTitle() + ": " + item.getPermalink()
-         catch e
-           console.log(e)
-           msg.send "Something's gone awry"


Only honor the part of selection that overlaps the current asset

Fixes #3865, maybe.  Needs testing with some video-enabled browsers.

--- web/volume/slot.coffee
+++ web/volume/slot.coffee
@@ -106,7 +106,7 @@
           seekOffset(o)
           unless ruler.selection.contains(o) || ruler.selection.u == o # "loose" contains
             ruler.selection = new TimeSegment(null)
-            finalizeSelection()
+            updateSelection()
         return

       style: ->
@@ -192,7 +192,7 @@
           seekOffset(@l)
         else if @full
           seekOffset(undefined)
-        finalizeSelection()
+        updateSelection()
         event?.stopPropagation()

     ################################### Global state
@@ -206,6 +206,7 @@
       selection: new TimeSegment(if 'select' of target then target.select else null) # current temporal selection
       position: new TimePoint(Offset.parse(target.pos)) # current position, also asset seek point (should be within selection)
       zoomed: 'range' of target # are we currently zoomed in?
+    playrange = undefined # ruler.selection.intersect($scope.asset.segment)

     searchLocation = (url) ->
       url
@@ -289,9 +290,9 @@
             $scope.current.setPosition(ruler.position.o - o)
           else
             ruler.position.o = $scope.asset.segment.l + o
-            if ruler.selection.uBounded && ruler.position.o >= ruler.selection.u
+            if playrange?.uBounded && ruler.position.o >= playrange.u
               video[0].pause()
-              seekOffset(ruler.selection.l) if ruler.selection.lBounded
+              seekOffset(playrange.l)
         return
       ended: ->
         $scope.playing = 0
@@ -397,7 +398,7 @@

         blank.fillData() if blank && this == blank
         $scope.playing = 0
-        finalizeSelection()
+        updateSelection()
         updatePlayerHeight()
         autoplay = ap
         true
@@ -462,7 +463,7 @@
             new TimeSegment(Math.min(sel.l, pos), pos)
           else
             new TimeSegment(pos, Math.max(sel.u, pos))
-      finalizeSelection()
+      updateSelection()
       return

     $scope.dragSelection = (down, up, c) ->
@@ -480,7 +481,7 @@
           new TimeSegment(startPos)
         else
           new TimeSegment(null)
-      finalizeSelection() if up.type != 'mousemove'
+      updateSelection() if up.type != 'mousemove'
       return

     $scope.zoom = (seg) ->
@@ -494,11 +495,12 @@
       searchLocation($location.replace())
       return

-    $scope.updateSelection = finalizeSelection = ->
+    $scope.updateSelection = updateSelection = ->
       if editing
         return false if $scope.editing == 'position'
         $scope.editing = true
         $scope.current.updateExcerpt() if $scope.current?.excerpts
+      playrange = $scope.asset?.segment.intersect(ruler.selection)
       for t in $scope.tags
         t.update()
       for c in $scope.comments
@@ -1110,7 +1112,7 @@
         $scope.commentReply = if event
           this.select(event)
           this
-        finalizeSelection()
+        updateSelection()

     $scope.addComment = (message, replyTo) ->
       slot.postComment {text:message}, getSelection(), replyTo?.comment.id
@@ -1143,7 +1145,7 @@
     $scope.playing = 0
     Record.place()
     updateRange()
-    finalizeSelection()
+    updateSelection()

     if editing
       done = $scope.$on '$locationChangeStart', (event, url) ->


Remove scale control from the map

Remove scale control from the map

--- src/map.coffee
+++ src/map.coffee
@@ -82,7 +82,7 @@
             @map_controls =
                 title: new widgets.TitleControl()
                 zoom: L.control.zoom position: 'bottomright'
-                scale: L.control.scale imperial: false, maxWidth: 200
+                # scale: L.control.scale imperial: false, maxWidth: 200
                 # service_sidebar: @service_sidebar.map_control()
             @current_markers = {}


Update coffee/chosen.jquery.coffee

Update coffee/chosen.jquery.coffee

Address jQuery 1.9 removing $.browser

--- coffee/chosen.jquery.coffee
+++ coffee/chosen.jquery.coffee
@@ -7,9 +7,22 @@

 $.fn.extend({
   chosen: (options) ->
+    ua = navigator.userAgent.toLowerCase();
+
+    match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
+      /(webkit)[ \/]([\w.]+)/.exec( ua ) ||
+    	/(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
+    	/(msie) ([\w.]+)/.exec( ua ) ||
+    	ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
+    	[];
+
+    browser =
+      name: match[ 1 ] || ""
+    	version: match[ 2 ] || "0"
+
     # Do no harm and return as soon as possible for unsupported browsers, namely IE6 and IE7
     # Continue on if running IE document type but in compatibility mode
-    return this if $.browser.msie and ($.browser.version is "6.0" or  ($.browser.version is "7.0" and document.documentMode is 7 ))
+    return this if browser.name.msie and (browser.version is "6.0" or  (browser.version is "7.0" and document.documentMode is 7 ))
     this.each((input_field) ->
       $this = $ this
       $this.data('chosen', new Chosen(this, options)) unless $this.hasClass "chzn-done"


Test the Intensity configChart function.

--- test/unit/servicesSpec.coffee
+++ test/unit/servicesSpec.coffee
@@ -58,3 +58,52 @@
         done()
       # Fire the backend request.
       $httpBackend.flush()
+
+
+  describe 'Intensity Service', ->
+    # The qiprofile Intensity factory.
+    Intensity = null
+
+    beforeEach ->
+      # Enable the test services.
+      inject ['Intensity', (_Intensity_) ->
+        Intensity = _Intensity_
+      ]
+
+    it 'should configure the chart', ->
+      # Make the dummy input session.
+      session =
+        scan:
+          intensity:
+            # Max intensity is at session 10.
+            intensities: (30 - Math.abs(10 - i) for i in [1..32])
+         registration:
+          intensity:
+            # Dampen the scan intensity a bit.
+            intensities: (30 - (Math.abs(10 - i) * 1.2) for i in [1..32])
+
+      # Configure the chart.
+      config = Intensity.configureChart(session)
+      expect(config.data).to.exist
+      expect(config.data.length).to.equal(2)
+
+      # Verify the scan coordinates.
+      scan = _.find config.data, (dataSeries) -> dataSeries.key == 'Scan'
+      expect(scan).to.exist
+      expect(scan.values).to.exist
+      scanX = (coord[0] for coord in scan.values)
+      expect(scanX).to.eql([1..32])
+      scanY = (coord[1] for coord in scan.values)
+      expect(scanY).to.eql(session.scan.intensity.intensities)
+
+      # Verify the registration coordinates.
+      reg = _.find config.data, (dataSeries) -> dataSeries.key == 'Realigned'
+      expect(reg).to.exist
+      expect(reg.values).to.exist
+      regX = (coord[0] for coord in reg.values)
+      expect(regX).to.eql([1..32])
+      regY = (coord[1] for coord in reg.values)
+      expect(regY).to.eql(session.registration.intensity.intensities)
+
+      # Verify the x-axis values.
+      expect(config.xValues).to.eql([1..32])


Split out win32 variable values

Split out win32 variable values

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -8,14 +8,20 @@
 packageJson = require './package.json'

 module.exports = (grunt) ->
-  appName = if process.platform is 'win32' then 'Atom' else 'Atom.app'
   [major, minor, patch] = packageJson.version.split('.')
-  tmpDir = if process.platform is 'win32' then os.tmpdir() else '/tmp'
+  if process.platform is 'win32'
+    appName = 'Atom'
+    tmpDir = os.tmpdir()
+    installRoot = process.env.ProgramFiles
+  else
+    appName = 'Atom.app'
+    tmpDir = '/tmp'
+    installRoot = '/Applications'
+
   buildDir = grunt.option('build-dir') ? path.join(tmpDir, 'atom-build')
   shellAppDir = path.join(buildDir, appName)
   contentsDir = path.join(shellAppDir, 'Contents')
   appDir = path.join(contentsDir, 'Resources', 'app')
-  installRoot = if process.platform is 'win32' then process.env.ProgramFiles else '/Applications'
   installDir = path.join(installRoot, appName)

   coffeeConfig =


fixed grunt copy task

fixed grunt copy task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -115,12 +115,10 @@
         flatten: true
         cwd: 'bower_components/'
         src: [
-          '**/*.js'
+          'jquery/dist/*.js'
+          'simple-jekyll-search/dest/*.js'
+          'mousetrap/mousetrap.js'
           '!**/*.min.js'
-          '!**/Grunt*.js'
-          '!**/src/*.js'
-          '!**/*/*/*.js'
-
         ]
         dest: '_site/vendor/js/'
       } ]


Took out accidental change to Gruntfile.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -773,7 +773,7 @@
       options: {
         outdir: 'build/dev/uproxy/integration/'
         # Uncomment this for debugging
-        keepRunner: true,
+        # keepRunner: true,
       }
     }
   }


Stop combine clean and concurrent to prevent high IO cost

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -12,7 +12,6 @@
           install: true
           copy: false
     concurrent:
-      clean: [ 'clean:dist', 'clean:server' ]
       preServer: [ 'copy:bootstrap', 'compass:server', 'coffee:server' ]
       # preCompile: compile the files to optimize
       preCompile: [ 'copy:building', 'copy:dist',
@@ -172,11 +171,11 @@
         'uglify:generated', 'filerev', 'usemin', 'htmlmin' ]

   grunt.registerTask 'build', 'Build the code for production',
-      [ 'bower:install', 'concurrent:clean', 'copy:bootstrap'
+      [ 'bower:install', 'clean:dist', 'clean:server', 'copy:bootstrap'
         'compile', 'clean:building', 'clean:cache' ]

   grunt.registerTask 'server', 'Start a preview server',
-      [ 'concurrent:clean', 'concurrent:preServer'
+      [ 'clean:dist', 'clean:server', 'concurrent:preServer'
         'configureProxies:server', 'connect:server', 'watch' ]

   grunt.registerTask 'default', 'UT (when has) & build',


set session cookie to external domains

set session cookie to external domains

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -27,6 +27,9 @@
 			'/sso/intranet': (req, res, next) ->
 				# TODO: check Authorization header
 				res.setHeader "Content-Type", "application/json; charset=UTF-8"
+				expires = new Date()
+				expires.setTime(expires.getTime() + 3600000)
+				res.setHeader "Set-Cookie", "_my_panel_session_token=1234567; path=/; domain=.panel.my-webapp.com; expires=#{expires.toGMTString()}"
 				res.end JSON.stringify({location_href: "http://local-intranet.panel.my-webapp.com:9001/"})
 				return
 		OPTIONS: