RemoteSubGraph: emit ready when all ports in graph are availablE

--- src/RemoteSubGraph.coffee
+++ src/RemoteSubGraph.coffee
@@ -8,6 +8,7 @@

     @runtime = null
     @ready = false
+    @graph = null

     @inPorts = new noflo.InPorts
     @outPorts = new noflo.OutPorts
@@ -44,8 +45,10 @@
         throw new Error "runtime #{@definition.id} does not declare protocol:runtime"
       if definition.graph
         noflo.graph.loadFile definition.graph, (graph) =>
+          @graph = graph
+          @runtime.setMain graph
           connection.sendGraph graph, @runtime, =>
-            @runtime.setMain graph
+            # ignore

     # TODO: make runtime base handle ports discovery similar to capabilities?
     portsRecv = 0
@@ -64,15 +67,26 @@
     @runtime.connect()

   setupPorts: (ports) ->
+    if @definition.graph and not @graph
+      # We are going to load and send a new graph to runtime, disregard whatever the runtime
+      # tells initially
+      return
+
+    if @graph
+      # We should only emit ready once the remote runtime sent us at least all the ports that
+      # the graph exports
+      for exported, metadata of @graph.inports
+        matching = ports.inPorts.filter (port) -> port.id is exported
+        return unless matching.length
+      for exported, metadata of @graph.outports
+        matching = ports.outPorts.filter (port) -> port.id is exported
+        return unless matching.length
+
     @setReady false
-    if ports.inPorts.length is 0 and ports.outPorts.length is 0
-      return
     # Expose remote graph's exported ports as node ports
     @prepareInport port for port in ports.inPorts
     @prepareOutport port for port in ports.outPorts
-    setTimeout =>
-      @setReady true
-    , 100
+    @setReady true

   prepareInport: (definition) ->
     name = definition.id


unbind geolocate

unbind geolocate

--- js/models/geo-locate-control.coffee
+++ js/models/geo-locate-control.coffee
@@ -9,6 +9,7 @@
       @instance = new OpenLayers.Control.Geolocate(
         {
         id: 'locate-control'
+        bind: false
         geolocationOptions:
           {
           enableHighAccuracy: false
@@ -18,11 +19,12 @@
         }
       )

-      @instance.events.register "locationupdated", @instance, (e) ->
+      @instance.events.register "locationupdated", @, (e) ->
         utils.currentLocation = {
           lat: e.position.coords.latitude
           lon: e.position.coords.longitude
         }
+
         utils.geoLocateLayer.removeAllFeatures()
         utils.geoLocateLayer.addFeatures([
           new OpenLayers.Feature.Vector(e.point


make listInitializedUser call heracles directly

--- js/src/http/UserDirectoryApi.coffee
+++ js/src/http/UserDirectoryApi.coffee
@@ -20,7 +20,7 @@

   getUserUrl   = -> Configuration.get('heraclesUrlV2') + '/directory/user'
   getUsersUrl  = -> Configuration.get('heraclesUrlV2') + '/directory/users'
-  usersInRealmUrl = -> Configuration.get('servicesUrlV2') + '/directory'
+  usersInRealmUrl = -> Configuration.get('heraclesUrlV2') + '/directory'
   setFirstLoginUrl = -> Configuration.get('heraclesUrlV2') + '/directory/setlogin'

   log = Logger.get('UserDirectoryApi')


Fixed #3

Fixed #3

--- src/youtube-info.coffee
+++ src/youtube-info.coffee
@@ -46,9 +46,12 @@
         title = entry.title.$t
         date = entry.published.$t
         date = date.substr(0, date.indexOf("T"))
-        thumbs_up = Math.round(((r.average-r.min)/(r.max-r.min))*r.numRaters)
-        thumbs_down = r.numRaters - thumbs_up
-        msg.send "YouTube: #{title} (#{formatTime(entry.media$group.yt$duration.seconds)}, #{humanizeNumber(entry.yt$statistics.viewCount)} views, uploaded #{date}, #{humanizeNumber(thumbs_up)} thumbs up, #{humanizeNumber(thumbs_down)} thumbs down)"
+        if r
+          thumbs_up = Math.round(((r.average-r.min)/(r.max-r.min))*r.numRaters)
+          thumbs_down = r.numRaters - thumbs_up
+          msg.send "YouTube: #{title} (#{formatTime(entry.media$group.yt$duration.seconds)}, #{humanizeNumber(entry.yt$statistics.viewCount)} views, uploaded #{date}, #{humanizeNumber(thumbs_up)} thumbs up, #{humanizeNumber(thumbs_down)} thumbs down)"
+        else
+          msg.send "YouTube: #{title} (#{formatTime(entry.media$group.yt$duration.seconds)}, #{humanizeNumber(entry.yt$statistics.viewCount)} views, uploaded #{date}, no rating)"
       else
         msg.send "YouTube: error: #{video_hash} returned #{res.statusCode}: #{body}"


use atom-text-editor instead

use atom-text-editor instead

--- lib/atom-ternjs-config-view.coffee
+++ lib/atom-ternjs-config-view.coffee
@@ -30,9 +30,9 @@
     wrapper.appendChild(header)
     wrapper.appendChild(doc)
     for path in obj
-      item = document.createElement('input')
-      item.type = 'text'
-      item.value = path
+      item = document.createElement('atom-text-editor')
+      item.setAttribute('mini', true)
+      item.getModel().getBuffer().setText(path)
       wrapper.appendChild(item)
     wrapper


Add missing reference to this.

Add missing reference to this.

--- src/cache/instance.coffee
+++ src/cache/instance.coffee
@@ -2,7 +2,7 @@

 class Instance extends Impromptu.Cache
   run: (fn) ->
-    return get fn if @_cached
+    return @get fn if @_cached

     @set (err, results) =>
       if err then fn err else @get fn


Upcoming AntennaPod subscription scheme

Upcoming AntennaPod subscription scheme

--- src/coffee/clients.coffee
+++ src/coffee/clients.coffee
@@ -37,7 +37,7 @@
   android: [
     #{
       #title: 'AntennaPod'
-      #scheme: 'pcast://'
+      #scheme: 'antennapod-subscribe://'
       #icon: 'android/antennapod@2x.png'
       #install: 'https://play.google.com/store/apps/details?id=de.danoeh.antennapod'
     #},


rm unusued pusher :hatched_chick:

rm unusued pusher :hatched_chick:

--- lib/eventemitter/pusher.coffee
+++ lib/eventemitter/pusher.coffee
@@ -1,36 +0,0 @@
-'use strict'
-
-module.exports = do->
-  getPusherEvent =(event)->
-    if Array.isArray(event)
-      event = event.join ':'
-    else event
-
-  {defineProperty} = Object
-
-  afterInit:do ->
-    channels = {}
-    ->
-      {broadcastable} = @constructor
-      id = @getId?() or @bongo_?.instanceId
-      if broadcastable and id?
-        name = "object-#{id}"
-        defineProperty @, 'channel'
-          value: channels[name] or= @mq.subscribe name
-        @channel.bind 'updateInstance', (data)=> @update_(data)
-
-  destroy:->
-    @mq.unsubscribe "object-#{id}"
-
-  on:(event, listener)->
-    {constructor} = @
-    event = getPusherEvent(event)
-    multiplex = @multiplexer.on event, ->
-      constructor.wrapArgs [].slice.call(arguments), (args)-> listener args...
-    @channel.bind event, multiplex if multiplex
-
-  off:(event, listener)->
-    # event = getPusherEvent(event)
-    # listenerCount = @multiplexer.off event, listener
-    # if listenerCount is 0
-    #   @channel.unbind event, @multiplexer.events[event]


Update Perl snippets for inserting GetOptions

--- src/configs/snippets.cson
+++ src/configs/snippets.cson
@@ -296,11 +296,11 @@
 		prefix: "say"
 		body: 'use feature "say";'
 	"Getopt: Import":
-		prefix: "opt"
+		prefix: "getopt"
 		body: "use Getopt::Long qw(:config auto_abbrev);"
 	"Getopt: Usage":
-		prefix: "getopt"
-		body: 'GetOptions(${1:"number=i" => $number, "string=s" => $string, "boolean"  => $bool});'
+		prefix: "getopts"
+		body: 'GetOptions(${1:"number=i" => \\$number, "string=s" => \\$string, "boolean" => \\$bool});'
 	Slurp:
 		prefix: "slurp"
 		body: """


Move check for if "Try Another" is valid into store

--- src/flux/task-step.coffee
+++ src/flux/task-step.coffee
@@ -2,7 +2,7 @@
 _ = require 'underscore'
 camelCase = require 'camelcase'
 flux = require 'flux-react'
-
+Task = require './task'
 {CrudConfig, makeSimpleStore, extendConfig} = require './helpers'

 TaskStepConfig =
@@ -85,6 +85,14 @@

       step.content.questions?[0].formats?.indexOf('free-response') > -1

+    canTryAnother: (id) ->
+      step = @_get(id)
+      step? and
+        (step.has_recovery and step.correct_answer_id isnt step.answer_id) and
+        not Task.TaskStore.isTaskPastDue(step.task_id) and
+        not @exports.isLoading.call(this, id) and
+        not @exports.isSaving.call(this, id)
+
 extendConfig(TaskStepConfig, new CrudConfig())
 {actions, store} = makeSimpleStore(TaskStepConfig)
 module.exports = {TaskStepActions:actions, TaskStepStore:store}


STATS : Fix tooltips on hover

STATS : Fix tooltips on hover

--- src/frontend/stats.coffee
+++ src/frontend/stats.coffee
@@ -26,9 +26,11 @@
             if @prevPoint isnt item.dataIndex
                 @prevPoint = item.dataIndex
                 do $("#tooltip").remove
-                x = item.datapoint[0].toFixed 2
-                y = item.datapoint[1].toFixed 2
-                @showTooltip item.pageX, item.pageY, item.series.label + " number on " + Date(x) + " = " + y
+                x = item.datapoint[0]
+                y = item.datapoint[1]
+                date = new Date(x)
+                x = do date.toLocaleDateString
+                @showTooltip item.pageX, item.pageY, item.series.label + "' number on " + x + " : " + y
         else
             do $("#tooltip").remove
             @prevPoint = null


Include decoration mixin in minimap editor view

--- lib/minimap-editor-view.coffee
+++ lib/minimap-editor-view.coffee
@@ -1,12 +1,14 @@
 {EditorView, ScrollView, $} = require 'atom'
 {Emitter} = require 'emissary'
 Delegato = require 'delegato'
+DecorationManagement = require './mixins/decoration_management'
 Debug = require 'prolix'

 module.exports =
 class MinimapEditorView extends ScrollView
   Emitter.includeInto(this)
   Delegato.includeInto(this)
+  DecorationManagement.includeInto(this)
   Debug('minimap').includeInto(this)

   @delegatesProperty 'firstRenderedScreenRow', toMethod: 'getFirstVisibleScreenRow'


Update PackageDetailView when package install state changes

--- lib/package-detail-view.coffee
+++ lib/package-detail-view.coffee
@@ -80,6 +80,9 @@
     else
       @packageRepo.hide()

+    @updateInstalledState()
+
+  updateInstalledState: ->
     @sections.empty()

     if @isInstalled()
@@ -95,6 +98,9 @@

   subscribeToPackageManager: ->
+    @subscribe @packageManager, 'theme-installed package-installed theme-install-failed package-install-failed', (pack) =>
+      @updateInstalledState()
+
     @subscribe @packageManager, 'theme-updated package-updated', (pack, newVersion) =>
       return unless @pack.name is pack.name


add test on creating incidents for registered components

--- test/cachet-test.coffee
+++ test/cachet-test.coffee
@@ -104,3 +104,21 @@
       helper.converse @robot, @user, '/incident fixed on foo: msg', (envelope, response) ->
         assert.include response, 'Incident `#126` declared'
         done()
+
+    it 'should allow to create incidents on known components', (done) ->
+      helper.converse @robot, @user, '/cachet component set foo 3', (envelope, response) ->
+        assert.include response, 'The component \'foo\' (id = 3) has been set'
+
+      json = {
+        name:"foo is back!",
+        message:"msg",
+        status:4,
+        component_id:3,
+        notify:true
+      }
+
+      api.post('/incidents', json).reply(201, { data: { id: 127 } })
+
+      helper.converse @robot, @user, '/incident fixed on foo: msg', (envelope, response) ->
+        assert.include response, 'Incident `#127` declared'
+        done()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/core_object.coffee
+++ test/core_object.coffee
@@ -39,7 +39,7 @@

   ret = o.include M

-  equals ret, o, "Should return lef"
+  equals ret, o, "Should return self"

   equals o.test(), "my_val"
   equals o.test2, "cool"


engine data module

engine data module

--- test/engine_data.coffee
+++ test/engine_data.coffee
@@ -4,6 +4,8 @@
   engine = Engine
     backgroundColor: false

-  engine.data.score =
+  engine.data.score = 0
+
+  engine.data.score

 module()


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/flickerable.coffee
+++ test/flickerable.coffee
@@ -14,4 +14,7 @@

   equals player.I.alpha, 0.2

+  4.times ->
+
+
 module()


Added more format unit tests

Added more format unit tests

--- test/format_test.coffee
+++ test/format_test.coffee
@@ -133,3 +133,49 @@
     test.equal formatted2, expected2, "Should render author"

     test.done()
+
+  "copyright and license": (test) ->
+    test.expect 3
+
+    parsed = [
+      {chalk: "overview", name:"Test", copyright:"Somone 2013"}
+    ]
+    formatted = chalkboard.format parsed
+    expected  = "\nTest\n===\n## Copyright\nSomone 2013\n"
+    test.equal formatted, expected, "Should render copyright"
+
+    parsed2 = [
+      {chalk: "overview", name:"Test", license:"MIT"}
+    ]
+    formatted2 = chalkboard.format parsed2
+    expected2  = "\nTest\n===\n## License\nMIT\n"
+    test.equal formatted2, expected2, "Should render license"
+
+    parsed3 = [
+      {chalk: "overview", name:"Test", license:"MIT", copyright:"Someone 2013"}
+    ]
+    formatted3 = chalkboard.format parsed3
+    expected3  = "\nTest\n===\n## Copyright and license\nSomeone 2013\n\nMIT\n"
+    test.equal formatted3, expected3, "Should render license and copyright"
+
+    test.done()
+
+  "other tags": (test) ->
+    tags = [
+      {tag: "default", value: "123", upper: "Default"}
+      {tag: "since", value: "yesterday", upper: "Since"}
+      {tag: "param", value: "123", upper: "Parameters"}
+      {tag: "TODO", value: "something", upper: "TODO"}
+      {tag: "example", value: "chalkboard hello", upper: "Example"}
+    ]
+    test.expect tags.length
+    nameOut = "\nTest\n===\n"
+    for tag in tags
+      parsed = [{chalk:"overview", name:"Test"}]
+      parsed[0][tag.tag] = tag.value
+
+      formatted = chalkboard.format parsed
+      expected  = "#{nameOut}### #{tag.upper}\n#{tag.value}\n"
+      test.equal formatted, expected, "Render @#{tag.tag} correctly"
+
+    test.done()


Use Math.max instead of Math.min

Use Math.max instead of Math.min

--- build/tasks/spec-task.coffee
+++ build/tasks/spec-task.coffee
@@ -78,7 +78,7 @@
       continue unless isAtomPackage(packagePath)
       packageSpecQueue.push(packagePath)

-    packageSpecQueue.concurrency = Math.min(1, concurrency - 1)
+    packageSpecQueue.concurrency = Math.max(1, concurrency - 1)
     packageSpecQueue.drain = -> callback(null, failedPackages)

   runCoreSpecs = (callback) ->


reivew

reivew

--- src-public/burn-down-chart/states/bdc/controller.coffee
+++ src-public/burn-down-chart/states/bdc/controller.coffee
@@ -1,27 +1,27 @@
 angular.module 'NotSoShitty.bdc'
 .controller 'BurnDownChartCtrl', ($scope, BDCDataProvider) ->
-  $scope.tableData = [
-    date: new Date(2015,11,30)
+  $scope.bdcData = [
+    date: new Date 2015, 11, 30
     standard: 0
     done: 0
   ,
-    date: new Date(2015,11,31)
+    date: new Date 2015, 11, 31
     standard: 8
     done: 12
   ,
-    date: new Date(2015,12,1)
+    date: new Date 2015, 12, 1
     standard: 16
     done: 13
   ,
-    date: new Date(2015,12,4)
+    date: new Date 2015, 12, 2
     standard: 22
     done: 24
   ,
-    date: new Date(2015,12,5)
+    date: new Date 2015, 12, 3
     standard: 30
     done: 28
   ,
-    date: new Date(2015,12,6)
+    date: new Date 2015, 12, 4
     standard: 40
     done: null
   ]


Added log when headers are created

Added log when headers are created

--- source/Downloader.coffee
+++ source/Downloader.coffee
@@ -31,3 +31,4 @@
       'Accept': '*/*'
       'Referer': 'http://www.123savemp3.net'
       'User-agent': 'Mozilla/5.0 (Macintosh)'
+  winston.info('Created download headers.')


Minor format fix

Minor format fix

--- library/exposure/access.coffee
+++ library/exposure/access.coffee
@@ -72,16 +72,16 @@
     # defined the appropriate getter property on supplied container.
     # If the entity or session does not exist, nothing gets defined.
     dereference: (container, callback) ->
-        skey = "x-authenticate-entity"; ckey = "entity"
+        sid = "x-authenticate-entity"; key = "entity"
         return callback() unless session = container.session
-        return callback() unless content = session[skey]
-        delete container[ekey] if _.has container, ekey
+        return callback() unless content = session[sid]
+        delete container[key] if _.has container, key
         @ressurectEntity ?= (xc, xn) -> xn null, xc
         @ressurectEntity content, (error, entity) ->
             format = (e) -> "ressurection error: #{e}"
             return callback format error if error
             p = get: -> return entity or undefined
-            Object.defineProperty container, ekey, p
+            Object.defineProperty container, key, p
             callback undefined; return this

     # Authenticate supplied entity as the authorized entity against


Substantial refactoring of provider wrapping

--- library/membrane/duplex.coffee
+++ library/membrane/duplex.coffee
@@ -108,18 +108,28 @@
     @provider: (method) ->
         noMethod = "a #{method} is not a function"
         invalidArgs = "has to have at least 1 parameter"
+        applicator = _.partial @covering, method
         assert _.isFunction(method), noMethod
         assert method.length >= 1, invalidArgs
+        method.provider = Object.create {}
+        method.providing = applicator
+        method.origin = this; method
+
+    # An important method that pertains to the details of internal
+    # duplex implementation. This method is used to produce a wrapper
+    # around the provider invocation procedure. This wrapping is of
+    # protective nature. It also exposes some goodies for the provider.
+    # Such as Socket.IO handle, session if available and the context.
+    @covering: (method, socket, context) -> (parameters..., callback) ->
         assert _.isFunction o = Marshal.serialize
         assert _.isFunction i = Marshal.deserialize
-        method.provider = Object.create {}
-        method.providing = (socket) -> (args..., callback) ->
-            guarded = @constructor.guarded? method, socket
-            assert _.isFunction g = guarded.run.bind guarded
-            execute = (a...) => g => method.apply this, i(a)
-            respond = (a...) => g => callback.apply this, o(a)
-            respond.socket = socket; execute args..., respond
-        method.origin = this; return method
+        guarded = @constructor.guarded? method, socket
+        assert _.isFunction g = guarded.run.bind guarded
+        execute = (a...) => g => method.apply this, i(a)
+        respond = (a...) => g => callback.apply this, o(a)
+        respond.socket = socket; respond.context = context
+        respond.session = socket?.handshake?.session
+        return execute parameters..., respond

     # This server side method is called on the context prior to the
     # context being compiled and flushed down to the client site. The
@@ -199,7 +209,7 @@
             providing = value?.providing or null
             return unless _.isFunction providing
             assert _.isFunction(value), internal
-            bound = providing(socket).bind this
+            bound = providing socket, context
             socket.on name, (args..., callback) =>
                 sentence = @upstreamAsync "sentence", =>
                     bound.call this, args..., callback


fix the healthcare

fix the healthcare

--- library/membrane/health.coffee
+++ library/membrane/health.coffee
@@ -98,9 +98,9 @@
     # to a summary and a value to a success or failure. Also, if any
     # error happens in any of the hertbeats, it reports to callback.
     healthcare: (callback) ->
-        adapt = (f) => @constructor.heartrate f
-        bind = (method) => adapt method.bind this
-        assert heartbeats = @constructor.heartbeat()
+        adapt = (f) => return @constructor.heartrate f
+        bind = (method) => return adapt method.bind this
+        heartbeats = @constructor.heartbeat() or Array()
         assert indexed = _.indexBy heartbeats, "summary"
         assert id = try @constructor.identify().underline
         transform = (a, v, k) -> a[k] = bind v.estimate


move regexp extensions to the end

move regexp extensions to the end

--- library/nucleus/extends.coffee
+++ library/nucleus/extends.coffee
@@ -47,37 +47,6 @@
 # placed in this class in order to transfer it to the remote call site.
 # Some of them depend on other subsystems, such as dynamic composition.
 module.exports.Extending = remote -> class Extending extends Object
-
-    # Extend the native RegExp object to implement method for escaping
-    # a supplied string. Escaping here means substituting all the RE
-    # characters so that it can be used inside of the regular expression
-    # pattern. The implementation was borrowed from StackOverflow thread.
-    RegExp.escape = (string) ->
-        noString = "please supply valid input"
-        assert not _.isEmpty(string), noString
-        primary = /[-\/\\^$*+?.()|[\]{}]/g
-        string.replace primary, "\\$&"
-
-    # Extend the native RegExp object to implement method for unescaping
-    # a supplied string. Unscaping here means substituting all the RE back
-    # characters so that it cannot be used inside of the regular expression
-    # pattern. The implementation was borrowed from StackOverflow thread.
-    RegExp::unescape = ->
-        noString = "cannot retrieve RE source"
-        assert not _.isEmpty(@source), noString
-        string = @source.replace /\\\//g, "/"
-        return string.replace /[\$\^]/g, ""
-
-    # Collect all the matches of the regular expression against of the
-    # supplied string. This method basically gathers all the matches that
-    # sequentially matches against the input string and packs them into
-    # an array which is handed to the invoker. Be sure to set the G flag.
-    RegExp::collect = (string) ->
-        matches = new Array undefined
-        noString = "got no string supplied"
-        assert _.isString(string), noString
-        matches.push m while m = @exec string
-        assert matches; return matches

     # Determine if the object that is bound to this invocation is a
     # subclass of the supplied archetype class (as argument). Of course
@@ -144,3 +113,34 @@
             assert _.isBoolean(boolean), wrong
             return @$abstract = this if boolean
             delete @$abstract; @$abstract is @
+
+    # Extend the native RegExp object to implement method for escaping
+    # a supplied string. Escaping here means substituting all the RE
+    # characters so that it can be used inside of the regular expression
+    # pattern. The implementation was borrowed from StackOverflow thread.
+    RegExp.escape = (string) ->
+        noString = "please supply valid input"
+        assert not _.isEmpty(string), noString
+        primary = /[-\/\\^$*+?.()|[\]{}]/g
+        string.replace primary, "\\$&"
+
+    # Extend the native RegExp object to implement method for unescaping
+    # a supplied string. Unscaping here means substituting all the RE back
+    # characters so that it cannot be used inside of the regular expression
+    # pattern. The implementation was borrowed from StackOverflow thread.
+    RegExp::unescape = ->
+        noString = "cannot retrieve RE source"
+        assert not _.isEmpty(@source), noString
+        string = @source.replace /\\\//g, "/"
+        return string.replace /[\$\^]/g, ""
+
+    # Collect all the matches of the regular expression against of the
+    # supplied string. This method basically gathers all the matches that
+    # sequentially matches against the input string and packs them into
+    # an array which is handed to the invoker. Be sure to set the G flag.
+    RegExp::collect = (string) ->
+        matches = new Array undefined
+        noString = "got no string supplied"
+        assert _.isString(string), noString
+        matches.push m while m = @exec string
+        assert matches; return matches


added test button

added test button

--- lib/decoration-example-view.coffee
+++ lib/decoration-example-view.coffee
@@ -16,6 +16,7 @@
         @div class: 'btn-group', =>
           @button outlet: 'highlightToggle', class: 'btn', 'Add highlight'
           @button outlet: 'removeHighlight', class: 'btn', 'Remove highlight'
+          @button outlet: 'testButton', class: 'btn', 'Test'
           #@button outlet: 'highlightColorCycle', class: 'btn', 'Cycle Highlight Color'

   colors: ['green', 'blue', 'red']
@@ -32,6 +33,7 @@
     #@gutterToggle.on 'click', => @toggleDecorationForCurrentSelection('gutter')
     @highlightToggle.on 'click', => @createDecorationForCurrentSelection('highlight')
     @removeHighlight.on 'click', => @destroyDecorationsAtCursor()
+    @testButton.on 'click', => @testFunction()

     #@lineColorCycle.on 'click', => @cycleDecorationColor('line')
     #@gutterColorCycle.on 'click', => @cycleDecorationColor('gutter')
@@ -196,7 +198,10 @@
       console.log "RANGEX", new Range(serialized_highlight.range)
       self.createHighlight(serialized_highlight.color, Range.deserialize(serialized_highlight.range))

-
+  testFunction: ->
+    commenting = require('./commenting.coffee')
+    console.log('hey you clicked the Test button:')
+    console.log(commenting.commentString(@getEditor()))

 ### !@#$%>haightlighter infos


fix inverted bool

fix inverted bool

--- editor-app/app.coffee
+++ editor-app/app.coffee
@@ -98,7 +98,7 @@
     .onValue (author) -> storage.open(author, initialApplication.name)

   isOwnerP = author.authorP.map (author) ->
-    !initialApplication.author || initialApplication.author isnt author
+    !initialApplication.author || initialApplication.author is author

   dirtyP = changedByUser.map(true)
     .merge(saveResultE.map(false))


Remove unused options and throw errors when callbacks are required

--- src/leveldb/handle.coffee
+++ src/leveldb/handle.coffee
@@ -8,7 +8,7 @@
   if typeof options is 'function'
     callback = options
     options = null
-  callback ?= noop
+  throw new Error 'Missing callback' unless callback
   binding.open path, options, (err, self) ->
     return callback err if err
     callback null, new Handle self
@@ -43,6 +43,7 @@
     if typeof options is 'function'
       callback = options
       options = null
+    throw new Error 'Missing callback' unless callback
     @self.get key, options, callback or noop
     @

@@ -80,11 +81,12 @@
   batch: ->
     new Batch @self

-  iterator: (options) ->
-    new Iterator @self.iterator options

-  snapshot: (options) ->
-    new Snapshot @, @self.snapshot options
+  iterator: ->
+    new Iterator @self.iterator()
+
+  snapshot: ->
+    new Snapshot @, @self.snapshot()

   property: (name) ->
     @self.property(name)


Fix space.

Fix space.

--- source/components.coffee
+++ source/components.coffee
@@ -1,4 +1,4 @@
-MemoryStore = require './stores/ memory.coffee'
+MemoryStore = require './stores/memory.coffee'
 Utils       = require './utils.coffee'

 # The main entry point for the framework.


changed background and updated co-ordinates


fixes forced-completions with only whitespace as prefix

--- lib/atom-ternjs-autocomplete.coffee
+++ lib/atom-ternjs-autocomplete.coffee
@@ -35,6 +35,9 @@
         if (!(/^[a-z0-9.\"\' ]$/i).test(prefix[prefix.length - 1]) or prefix.indexOf('..') != -1) and !@force
             @documentationView.hide()
             return []
+
+        if (!prefix.replace(/\s/g, '').length) or prefix.endsWith(';')
+            prefix = ''

         that = this


Renamed req to xhr

Renamed req to xhr

--- source/micro.ajax.coffee
+++ source/micro.ajax.coffee
@@ -37,23 +37,23 @@

     options.data = @serialize options

-    req = new XMLHttpRequest()
+    xhr = new XMLHttpRequest()

-    req.open options.type, options.url, options.async
+    xhr.open options.type, options.url, options.async

-    req.onload = ->
-      if req.status >= 200 and req.status < 400
-        data = _parseResponse req, options
+    xhr.onload = ->
+      if xhr.status >= 200 and xhr.status < 400
+        data = _parseResponse xhr, options
         if options.cache then _cacheRequest data, options
         options.success data

-    req.onerror = ->
+    xhr.onerror = ->
       options.error 'error'

-    _xhrHeaders req, options
+    _xhrHeaders xhr, options

-    req.send options.data
-    req
+    xhr.send options.data
+    xhr

   serialize: (options) ->
     data = options.data


increase time allowed for exiting ASpellWorker test from 100ms to 1s

--- test/unit/coffee/ASpellTests.coffee
+++ test/unit/coffee/ASpellTests.coffee
@@ -63,4 +63,4 @@
 		# or the CI server.
 		it "should return in reasonable time", () ->
 			delta = Date.now()-@start
-			delta.should.be.below(@ASpell.ASPELL_TIMEOUT + 100)
+			delta.should.be.below(@ASpell.ASPELL_TIMEOUT + 1000)


update version

update version

--- test/unit-module.coffee
+++ test/unit-module.coffee
@@ -7,4 +7,4 @@
   describe 'module', ->

     it 'version', ->
-      expect( domaintree.VERSION ).to.equal '1.0.0'
+      expect( domaintree.VERSION ).to.equal '0.1.1-3'


install_spec.coffee: Use deduped node-gyp path

--- spec/install-spec.coffee
+++ spec/install-spec.coffee
@@ -531,13 +531,13 @@
         expect(json[1].metadata.name).toBe 'test-module2'

     describe "with a space in node-gyp's path", ->
-      npmNodeModules = fs.realpathSync(path.join(__dirname, '..', 'node_modules', 'npm', 'node_modules'))
+      nodeModules = fs.realpathSync(path.join(__dirname, '..', 'node_modules'))

       beforeEach ->
         # Normally npm_config_node_gyp would be ignored, but it works here because we're calling apm
         # directly and not through the scripts in bin/
-        fs.copySync path.join(npmNodeModules, 'node-gyp'), path.join(npmNodeModules, 'with a space')
-        process.env.npm_config_node_gyp = path.join(npmNodeModules, 'with a space', 'bin', 'node-gyp.js')
+        fs.copySync path.join(nodeModules, 'node-gyp'), path.join(nodeModules, 'with a space')
+        process.env.npm_config_node_gyp = path.join(nodeModules, 'with a space', 'bin', 'node-gyp.js')

         # Read + execute permission
         fs.chmodSync(process.env.npm_config_node_gyp, fs.constants.S_IRUSR | fs.constants.S_IXUSR)


excise outdated comment

excise outdated comment

--- spec/project-spec.coffee
+++ spec/project-spec.coffee
@@ -200,11 +200,6 @@
             expect(anotherBuffer).not.toBe buffer

   describe ".repositoryForDirectory(directory)", ->
-    # it() causes beforeEach() in spec-helper.coffee to be run, which reassigns
-    # atom.project. Therefore, the atom.project used in waitsForPromise() will
-    # be different than the atom.project used in a subsequent it(). To work
-    # around this issue, we use runs() instead of it().
-
     it "there is no repository for /tmp even though there are RepositoryProviders", ->
       the_result = "dummy_value"


operations: Notify notifies w/ node

operations: Notify notifies w/ node

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -1218,6 +1218,7 @@
                 if req.length > 0
                     for listener in otherListeners
                         notification = Object.create(req)
+                        notification.node = @req.node
                         notification.listener = listener
                         @router.notify notification
             cb2()


Add yield sign for the US

Add yield sign for the US

Fixes mapillary/traffico#21

--- dev/us.cson
+++ dev/us.cson
@@ -12,6 +12,15 @@
   elements: [
     { type: 'octagon', color: 'red' }
     { type: 'stop', color: 'white' }
+  ]
+
+regulatory_yield:
+  category: 'regulatory'
+  name: 'give way'
+  elements: [
+    { type: 'tri-rounded', color: 'red', transform: 'rotate(180deg)' }
+    { type: 'tri-rounded', color: 'white', transform: '{tri2center} scale(.55) {center2tri} rotate(180deg)' }
+    { type: 'content-4', content: 'YIELD', color:'red', transform: 'translate(0,-115%) scale(.6)'}
   ]

 warning_merge:


added context

added context

--- review_front/app/components/copy-to-clipboard.coffee
+++ review_front/app/components/copy-to-clipboard.coffee
@@ -12,7 +12,7 @@
   actions:
     afterCopy: ->
       @justCopiedToggle()
-      Ember.run.later (->
+      Ember.run.later @, (->
         @justCopiedToggle()
       ), 1500


Shorthand notation for constructing LocationRanges

--- src/trix/models/location_range.coffee
+++ src/trix/models/location_range.coffee
@@ -17,12 +17,21 @@

     new this start, end

-  constructor: (@start, @end) ->
+  constructor: (start, end) ->
+    @start = parse(start)
+    @end = parse(end)
+
     {@index, @offset} = @start

     unless @end?
       @end = {}
       @end[key] = val for key, val of start
+
+  parse = (location) ->
+    if Array.isArray(location)
+      index: location[0], offset: location[1]
+    else
+      location

   isEqualTo: (locationRange) ->
     @start.index is locationRange?.start?.index and


cache-control: no-cache header set

cache-control: no-cache header set

--- rest_controller.coffee
+++ rest_controller.coffee
@@ -151,7 +151,12 @@
     models = if _.isArray(json) then _.map(json, (model_json) => new @model_type(@model_type::parse(model_json))) else new @model_type(@model_type::parse(json))
     JSONUtils.renderTemplate models, template, options, callback

+  setHeaders: (req, res, next) =>
+    res.header('Cache-Control', 'no-cache')
+    next()
+
   _call: (fn) =>
     auths = if _.isArray(@auth) then @auth.slice() else if @auth then [@auth] else []
+    auths.push(@setHeaders)
     auths.push(fn)
     return auths


Backspace is working on mobile -- we have terminals!

--- salvus/page/console.coffee
+++ salvus/page/console.coffee
@@ -187,6 +187,10 @@
                 if changeObj.next?
                     handle_mobile_change(ed, changeObj.next)
             editor.on('change', handle_mobile_change)
+
+            @mobile_keydown = (ev) =>
+                if ev.keyCode == 8
+                    @terminal.keyDown(ev)

             # Buttons
             @element.find(".salvus-console-up").click () ->
@@ -247,6 +251,9 @@

     blur : () =>
         @is_focused = false
+        if IS_MOBILE
+            $(document).off('keydown', @mobile_keydown)
+
         @terminal.blur()
         $(@terminal.element).removeClass('salvus-console-focus').addClass('salvus-console-blur')
         editor = @terminal.editor
@@ -257,8 +264,11 @@

     focus : () =>
         @is_focused = true
-        if not IS_MOBILE
+        if IS_MOBILE
+            $(document).on('keydown', @mobile_keydown)
+        else
             @terminal.focus()
+
         $(@terminal.element).addClass('salvus-console-focus').removeClass('salvus-console-blur')
         editor = @terminal.editor
         if editor?


i forgot to checkin the new help text for copy/paste.

--- salvus/page/console.coffee
+++ salvus/page/console.coffee
@@ -421,7 +421,7 @@
             return false

         @element.find("a[href=#paste]").click () =>
-            bootbox.alert("To copy, highlight text then press Control+C (or Command+C).  Press Control+V (or Command+V) to paste.  When no text is highlighted, Control+C sends the usual interrupt signal.")
+            bootbox.alert("To copy, highlight text then press ctrl+c (or command+c).  Press ctrl+v (or command+v) to paste.  When no text is highlighted, ctrl+c sends the usual interrupt signal.")
             return false

     _init_input_line: () =>


issue with message when copying files between projects

--- salvus/page/project.coffee
+++ salvus/page/project.coffee
@@ -2266,7 +2266,7 @@
                         src_path          = dialog.find(".salvus-project-copy-src-path").val()
                         selector          = dialog.find(".salvus-project-target-project-id")
                         target_project_id = selector.val()
-                        title_project     = selector.find('option[value="#{target_project_id}"]:first').text()
+                        target_project    = selector.find("option[value='#{target_project_id}']:first").text()
                         target_path       = dialog.find(".salvus-project-copy-target-path").val()
                         overwrite_newer   = dialog.find(".salvus-project-overwrite-newer").is(":checked")
                         delete_missing    = dialog.find(".salvus-project-delete-missing").is(":checked")


Update Server.coffee

Update Server.coffee

--- services/web/app/coffee/infrastructure/Server.coffee
+++ services/web/app/coffee/infrastructure/Server.coffee
@@ -38,6 +38,7 @@
 ErrorController = require "../Features/Errors/ErrorController"
 UserSessionsManager = require "../Features/User/UserSessionsManager"
 AuthenticationController = require "../Features/Authentication/AuthenticationController"
+

 metrics.event_loop?.monitor(logger)


call on change when clear

call on change when clear

--- src/ng-quick-date.coffee
+++ src/ng-quick-date.coffee
@@ -247,8 +247,7 @@
       scope.calendarDate = new Date(new Date(scope.calendarDate).setMonth(scope.calendarDate.getMonth() - 1))

     scope.clear = ->
-      scope.ngModel = null
-      scope.toggleCalendar(false)
+      scope.setDate(null, true)

     initialize()


dont show archived groups

dont show archived groups

--- vue/src/shared/models/user_model.coffee
+++ vue/src/shared/models/user_model.coffee
@@ -40,7 +40,7 @@

   groups: ->
     @recordStore.groups.collection.chain().
-      find(id: { $in: @groupIds() }).
+      find(id: { $in: @groupIds() }, archivedAt: null).
       simplesort('fullName').data()

   parentGroups: ->


better number formatting for zero

better number formatting for zero

--- src/number/format.coffee
+++ src/number/format.coffee
@@ -19,7 +19,9 @@
   if typeof key is "string" and time.indexOf(key.toLowerCase()) >= 0
     ret = number
   else if key is "share"
-    if number >= 100
+    if number is 0
+      ret = 0
+    else if number >= 100
       ret = d3.format(",f") number
     else if number > 99
       ret = d3.format(".3g") number
@@ -38,6 +40,8 @@
     ret = number + symbol
   else if length is 3
     ret = d3.format(",f") number
+  else if number is 0
+    ret = 0
   else
     ret = d3.format(".3g") number


add animation to modals

add animation to modals

--- addon/components/bs-modal/component.coffee
+++ addon/components/bs-modal/component.coffee
@@ -11,7 +11,9 @@
   classNameBindings: ['fade', 'isVis:in']
   attributeBindings: ['role', 'aria-labelledby', 'isAriaHidden:aria-hidden', 'ariaLabelledBy:aria-labelledby', 'style']
   style: (() ->
-    return Ember.String.htmlSafe('display: block')
+    if this.get('isVisible')
+      return Ember.String.htmlSafe('display: block;')
+    return
   ).property('isVisible')
   isAriaHidden: (() ->
     return "#{!@get('isVisible')}"
@@ -28,12 +30,22 @@
   fade: true
   action: null
   allowClose: true
-  isVis: true
+  animation: false
+  isVis: Ember.computed('isVisible', 'animation', {
+    get: () ->
+      if not @get('animation')
+        return @get('isVisible')
+      meta = Ember.meta(this);
+      cache = meta.readableCache();
+      return cache[key] || false
+    set: (key, val) ->
+      return val
+  })
   isVisible: Ember.computed({
     get: () -> return true
     set: (key, val, cache) ->
       if val == undefined
-        val = false
+        val = true
       if val == cache
         return val
       if val
@@ -45,34 +57,52 @@

   didInsertElement: ->
     @._super()
-    @setupBinders()
     if @get('isVisible')
-      @becameVisible()
+      Ember.run.scheduleOnce('afterRender', this, @becameVisible)
     return

   becameVisible: ->
     Em.$('body').addClass('modal-open')
     @appendBackdrop() if @get('backdrop')
+    @setupBinders()
+    @show()
     return

   becameHidden: ->
     Em.$('body').removeClass('modal-open')
-    @_backdrop.remove() if @_backdrop
+    @removeHandlers()
+    if @_backdrop
+      @_backdrop.remove()
+      @_backdrop = null
     return

   appendBackdrop: ->
-    parentElement = @$().parent()
-    @_backdrop = Em.$(@modalBackdrop).appendTo(parentElement)
+    if not @_backdrop
+      parentElement = @$().parent()
+      @_backdrop = Em.$(@modalBackdrop).appendTo(parentElement)
     return

-  hide: ->
-    @set('isVis', false)
-    current = this
-    @$().one('webkitTransitionEnd', (e) ->
-      current.set('isVisible', false)
-      return
+  show: () ->
+    return new Ember.RSVP.Promise((resolve) =>
+      if not @get('animation')
+        @set('isVis', true)
+        resolve()
+        return
     )
-    return false
+
+  hide: () ->
+    return new Ember.RSVP.Promise((resolve) =>
+      @set('isVis', false)
+      if not @get('animation')
+        @set('isVisible', false)
+        resolve()
+        return
+
+      @$().one('webkitTransitionEnd', (e) =>
+        @set('isVisible', false)
+        resolve()
+      )
+    )

   keyPressed: (event) ->
 #Handle ESC
@@ -81,8 +111,11 @@
     return

   close: (event) ->
-    @attrs.onClose?()
-    @get('modalManager').close()
+    @hide().then(() =>
+      @attrs.onClose?()
+      @get('modalManager').close()
+      return
+    )
     return

   actions: {
@@ -91,21 +124,22 @@
       return
   }

-
 #Invoked automatically by ember when the view is destroyed, giving us a chance to perform cleanups
   willDestroyElement: () ->
-    Em.$('body').removeClass('modal-open')
-    @removeHandlers()
-    @_backdrop.remove() if @_backdrop
+    @_super(arguments...)
+    @becameHidden()
     return

   removeHandlers: ->
 #Remove key press
     jQuery(window.document).unbind('keyup', @_keyUpHandler)
+    @_keyUpHandler = null
     return

   setupBinders: ->
 #Key press
+    if @_keyUpHandler
+      return
     handler = (event) =>
       @keyPressed(event)
       return


serviceName -> name

serviceName -> name

--- lib/dns_sd.coffee
+++ lib/dns_sd.coffee
@@ -92,12 +92,12 @@
       # event.address = service.address if service.type == 28
       continue unless service.class == 1 and service.type == 12
       continue unless service.data?
-      serviceName = service.data.split(".")[0].replace /\s\(\d+\)/, ""
+      name = service.data.split(".")[0].replace /\s\(\d+\)/, ""
       @_updateService
         address: rinfo.address
         hostname: null
-        serviceName: serviceName
-        path: "/printers/#{serviceName}/"
+        name: name
+        path: "/printers/#{name}/"

   _updateService: (service) =>
     # console.log service
@@ -109,7 +109,7 @@
     @emit "serviceUp", service

   _isSameService: (e1, e2) ->
-    e2.serviceName == e1.serviceName and e2.address == e1.address
+    e2.name == e1.name and e2.address == e1.address

   _removeService: (service) =>
     _.remove @services, _.partial(@_isSameService, service)


Read pane/item directly off tab element instead of looking up via DOM

Determining a tab’s pane or item via the DOM is unnecessary because we
store this state directly on each tab. It was leading to exceptions in
cases where a tab’s pane was being removed as a side-effect of dragging
because the tab didn’t have a parent element. This happens when the
‘core.removeEmptyPanes’ config setting is enabled.

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@github.com>
Signed-off-by: Antonio Scandurra <0e17b4d558b07b12cd221e35c438be378ba118cf@github.com>

--- lib/layout.coffee
+++ lib/layout.coffee
@@ -1,4 +1,4 @@
-{closest, indexOf, matches} = require './html-helpers'
+{closest, matches} = require './html-helpers'

 module.exports =

@@ -35,9 +35,9 @@
       when 'down'  then target.splitDown()
     tab = e.target
     toPane ?= target
-    fromPane = @paneForTab tab
+    fromPane = tab.pane
     return if toPane is fromPane
-    item = @itemForTab tab
+    item = tab.item
     fromPane.moveItemToPane item, toPane
     toPane.activateItem item
     toPane.activate()
@@ -51,14 +51,8 @@
   getPaneAt: (coords) ->
     @test.pane or @getElement(@lastCoords, 'atom-pane')?.getModel()

-  paneForTab: (tab) ->
-    tab.parentElement.pane
-
-  itemForTab: (tab) ->
-    @paneForTab(tab).getItems()[indexOf(tab)]
-
   isOnlyTabInPane: (pane, tab) ->
-    pane.getItems().length is 1 and pane is @paneForTab tab
+    pane.getItems().length is 1 and pane is tab.pane

   normalizeCoords: ({left, top, width, height}, [x, y]) ->
     [(x-left)/width, (y-top)/height]


:bug: Handle a possible error case

:bug: Handle a possible error case

--- lib/linter.coffee
+++ lib/linter.coffee
@@ -77,8 +77,6 @@
         continue if (not Result) or (typeof Result) isnt 'object'
         if Result instanceof Array
           Messages = Messages.concat(Result)
-        else
-          Messages.push Result
       if onChange
         @MessagesFly = Messages
       else


Added documentation for Readmill.Store.isSupported()

--- src/readmill/store.coffee
+++ src/readmill/store.coffee
@@ -25,6 +25,13 @@
   # Internal: Reference to the global localStorage object.
   @localStorage: window.localStorage

+  # Public: Checks to see if the current browser supports local storage.
+  #
+  # Examples
+  #
+  #   store = new Store if Store.isSupported()
+  #
+  # Returns true if the browser supports local storage.
   @isSupported: ->
     try "localStorage" in window && window["localStorage"] != null
     catch e then false


Added Store.isSupported() method

Added Store.isSupported() method

--- src/readmill/store.coffee
+++ src/readmill/store.coffee
@@ -14,7 +14,7 @@
 #   store.remove("name")
 #
 # Returns a new instance of Store.
-Annotator.Store = class Store extends Annotator.Class
+Annotator.Readmill.Store = class Store extends Annotator.Class

   # Internal: Prefix for all keys stored by the store.
   @KEY_PREFIX: "annotator.readmill/"
@@ -24,6 +24,10 @@

   # Internal: Reference to the global localStorage object.
   @localStorage: window.localStorage
+
+  @isSupported: ->
+    try "localStorage" in window && window["localStorage"] != null
+    catch e then false

   # Public: Get the current time as a unix timestamp in
   # milliseconds.
@@ -82,7 +86,7 @@
     try
       Store.localStorage.setItem @prefixed(key), value
     catch error
-      this.publish 'error', [error, key, value, this]
+      this.publish 'error', [error, this]
     this

   # Public: Removes the key from the localStorage.


minor cleanup #441

minor cleanup #441

--- lib/motion.coffee
+++ lib/motion.coffee
@@ -476,9 +476,10 @@
     foundPoint = null
     @editor.scanInBufferRange @sentenceRegex, scanRange, ({range, matchText, match, stop}) =>
       if match[1]?
-        return if @skipBlankRow and @isBlankRow(range.end.row)
-        if @isBlankRow(range.start.row) isnt @isBlankRow(range.end.row)
-          foundPoint = @getFirstCharacterPositionForRow(range.end.row)
+        {start: {row: startRow}, end: {row: endRow}} = range
+        return if @skipBlankRow and @isBlankRow(endRow)
+        if @isBlankRow(startRow) isnt @isBlankRow(endRow)
+          foundPoint = @getFirstCharacterPositionForRow(endRow)
       else
         foundPoint = range.end
       stop() if foundPoint?


llow only pdf and text for uploads

llow only pdf and text for uploads

--- src/routes/schemo.coffee
+++ src/routes/schemo.coffee
@@ -34,7 +34,12 @@
 						{
 							name: 'mediaType'
 							in: 'formData'
-							type: 'string'
+							schema:
+								$ref: '#/definitions/InfolisFile/mediaType'
+							enum: [
+								'application/pdf'
+								'text/plain'
+							]
 						}
 					]
 					responses:


Trying again

Trying again

--- src/scripts/sifter.coffee
+++ src/scripts/sifter.coffee
@@ -61,8 +61,7 @@

   setInterval ->
     users = robot.users
-    user = users[0].id
-    console.log user
+    console.log users[0]
   , 5000

 class Project


Add support for URLs with dash, add amzn.to custom domain

--- src/scripts/bitly.coffee
+++ src/scripts/bitly.coffee
@@ -30,7 +30,7 @@
         msg.send if response.status_code is 200 then response.data.url else response.status_txt

    #TODO: can we make this list more expansive/dynamically generated?
-   robot.hear /(https?:\/\/(bit\.ly|yhoo\.it|j\.mp|pep\.si)\/[0-9a-z]+)/ig, (msg) ->
+   robot.hear /(https?:\/\/(bit\.ly|yhoo\.it|j\.mp|pep\.si|amzn\.to)\/[0-9a-z\-]+)/ig, (msg) ->
     msg
       .http("http://api.bitly.com/v3/expand")
       .query


fixing dumb whitespace

fixing dumb whitespace

--- lib/parser.coffee
+++ lib/parser.coffee
@@ -20,7 +20,6 @@
   @LINES = /\r\n|\n/

   @GLOBAL_LINES = /\r\n|\n/g
-

   @LINE_HEAD_CHAR =
     js: /^\s*\*/,


And here too FES-10

And here too FES-10

--- lib/router.coffee
+++ lib/router.coffee
@@ -32,10 +32,9 @@

   res.setHeader 'Server', serverAgent

-  rewrittenUrl = conf.rewrite req.url
-
-  console.log "Rewriting", req.url, "to", rewrittenUrl
-  req.url = rewrittenUrl
+  if conf?
+    rewrittenUrl = conf.rewrite req.url
+    req.url = rewrittenUrl

   unless conf?
     res.setHeader 'Content-Type', 'text/html'


finally -- remove the hard disable of email sending

--- src/smc-hub/email.coffee
+++ src/smc-hub/email.coffee
@@ -65,9 +65,6 @@
         cb           : undefined
         category     : undefined
         asm_group    : undefined
-    # hard disabled for everything due to abuse
-    opts.cb?()
-    return

     if opts.verbose
         dbg = (m) -> winston.debug("send_email(to:#{opts.to}) -- #{m}")


Fix an error that may lead to [ERROR] Error: getaddrinfo ENOTFOUND undefined undefined:80

--- lib/update.coffee
+++ lib/update.coffee
@@ -3,13 +3,13 @@
 request = Promise.promisifyAll require 'request'
 requestAsync = Promise.promisify request, multiArgs: true

-{SERVER_HOSTNAME, POI_VERSION, ROOT} = global
+{POI_VERSION, ROOT} = global
 {log, warn, error} = require './utils'

 module.exports =
   checkUpdate: async (callback) ->
     try
-      [response, body] = yield requestAsync "http://#{SERVER_HOSTNAME}/update/latest.json",
+      [response, body] = yield requestAsync "http://#{global.SERVER_HOSTNAME}/update/latest.json",
         method: 'GET'
         json: true
         headers:


update to newer versions of electron

update to newer versions of electron

--- src/ui/views/menu.coffee
+++ src/ui/views/menu.coffee
@@ -7,22 +7,16 @@
 isNotDarwin = platform != 'darwin'

 acceleratorMap = {
-    hideyakyak: {default: 'Control+H', darwin:'Command+H'}
+    # MacOSX specific
+    hideyakyak: {default: 'CmdOrCtrl+H'}
     hideothers: {default: '', darwin:'Command+Shift+H'}
     showall: {default: '', darwin:''}
-    openinspector: {default: 'Control+Alt+I', darwin:'Command+Alt+I'}
-    quit: {default: 'Control+Q', darwin:'Command+Q'}
-    undo: {default: 'Control+Z', darwin:'Command+Z'}
-    redo: {default: 'Control+Shift+Z', darwin:'Command+Shift+Z'}
-    cut: {default: '', darwin:'Command+X'}
-    copy: {default: '', darwin:'Command+C'}
-    paste: {default: '', darwin:'Command+V'}
-    selectall: {default: '', darwin:'Command+A'}
-    togglefullscreen: {default: 'Control+Alt+F', darwin:'Command+Control+F'}
-    zoomin: {default: 'Control+Plus', darwin:'Command+Plus'}
-    zoomout: {default: 'Control+-', darwin:'Command+-'}
-    resetzoom: {default: 'Control+0', darwin:'Command+0'}
-    previousconversation: {default: 'Control+K', darwin:'Command+Shift+Tab'}
+    openinspector: {default: 'CmdOrCtrl+Alt+I'}
+    close: {default: '', darwin:'Command+W'}
+    # Common shortcuts
+    quit: {default: 'CmdOrCtrl+Q'}
+    # Platform specific
+    previousconversation: {default: 'Ctrl+K', darwin:'Command+Shift+Tab'}
     nextconversation:  {default: 'Control+J', darwin:'Command+Tab'}
     conversation1: {default: 'Alt+1', darwin:'Command+1'}
     conversation2: {default: 'Alt+2', darwin:'Command+2'}
@@ -33,8 +27,6 @@
     conversation7: {default: 'Alt+7', darwin:'Command+7'}
     conversation8: {default: 'Alt+8', darwin:'Command+8'}
     conversation9: {default: 'Alt+9', darwin:'Command+9'}
-    minimize: {default: '', darwin:'Command+M'}
-    close: {default: '', darwin:'Command+W'}
 }

 getAccelerator = (key) ->
@@ -46,7 +38,7 @@
 templateYakYak = (viewstate) ->

     [
-        { label: 'About YakYak', selector: 'orderFrontStandardAboutPanel:' } if isDarwin
+        { label: 'About YakYak', role: 'about' } if isDarwin
         #{ type: 'separator' }
         # { label: 'Preferences...', accelerator: 'Command+,',
         # click: => delegate.openConfig() }
@@ -54,26 +46,16 @@
         {
             label: 'Hide YakYak'
             accelerator: getAccelerator('hideyakyak')
-            selector: 'hide:' if isDarwin
-            role: 'minimize' if isNotDarwin
-        }
-        if isDarwin
-            {
-                label: 'Hide Others'
-                accelerator: getAccelerator('hideothers')
-                selector: 'hideOtherApplications:' if isDarwin
-            }
-
-            {
-                label: 'Show All'
-                selector: 'unhideAllApplications:' if isDarwin
-            }
-        { type: 'separator' }
-        {
-            label: 'Open Inspector'
-            accelerator: getAccelerator('openinspector')
-            click: -> action 'devtools'
-        }
+            role: (isDarwin ? 'hide' : 'minimize')
+        }
+        {
+            label: 'Hide Others'
+            accelerator: getAccelerator('hideothers')
+            role: 'hideothers'
+        } if isDarwin
+        { role: 'unhide' } if isDarwin # old show all
+        { type: 'separator' }
+        { role: 'toggledevtools' }
         { type: 'separator' }
         {
             label: 'Logout',
@@ -89,44 +71,13 @@

 templateEdit = (viewstate) ->
     [
-        {
-          label: 'Undo'
-          accelerator: getAccelerator('undo')
-          selector: 'undo:' if isDarwin
-          role: 'undo' if isNotDarwin
-        }
-        {
-          label: 'Redo'
-          accelerator: getAccelerator('redo')
-          selector: 'redo:' if isDarwin
-          role: 'redo' if isNotDarwin
-        }
-        if isDarwin
-            { type: 'separator' }
-            {
-              label: 'Cut'
-              accelerator: getAccelerator('cut')
-              selector: 'cut:' if isDarwin
-              role: 'cut' if isNotDarwin
-            }
-            {
-              label: 'Copy'
-              accelerator: getAccelerator('copy')
-              selector: 'copy:' if isDarwin
-              role: 'copy' if isNotDarwin
-            }
-            {
-              label: 'Paste'
-              accelerator: getAccelerator('paste')
-              selector: 'paste:' if isDarwin
-              role: 'paste' if isNotDarwin
-            }
-            {
-              label: 'Select All'
-              accelerator: getAccelerator('selectall')
-              selector: 'selectAll:' if isDarwin
-              role: 'selectall' if isNotDarwin
-            }
+        { role: 'undo' }
+        { role: 'redo' }
+        { type: 'separator' }
+        { role: 'cut' }
+        { role: 'copy' }
+        { role: 'paste' }
+        { role: 'selectall' }
     ].filter (n) -> n != undefined

 templateView = (viewstate) ->
@@ -140,25 +91,29 @@
                   checked: viewstate.showConvThumbs
                   enabled: viewstate.loggedin
                   click: (it) -> action 'showconvthumbs', it.checked
-              }, {
+              }
+              {
                   type: 'checkbox'
                   label: 'Show Thumbnails Only'
                   checked:viewstate.showConvMin
                   enabled: viewstate.loggedin
                   click: (it) -> action 'showconvmin', it.checked
-              }, {
+              }
+              {
                   type: 'checkbox'
                   label: 'Show Animated Thumbnails'
                   checked:viewstate.showAnimatedThumbs
                   enabled: viewstate.loggedin
                   click: (it) -> action 'showanimatedthumbs', it.checked
-              }, {
+              }
+              {
                   type: 'checkbox'
                   label: 'Show Conversation Timestamp'
                   checked:viewstate.showConvTime
                   enabled: viewstate.loggedin && !viewstate.showConvMin
                   click: (it) -> action 'showconvtime', it.checked
-              }, {
+              }
+              {
                   type: 'checkbox'
                   label: 'Show Conversation Last Message'
                   checked:viewstate.showConvLast
@@ -166,19 +121,22 @@
                   click: (it) -> action 'showconvlast', it.checked
               }
           ]
-        }, {
+        }
+        {
             type: 'checkbox'
             label: 'Show Pop-Up (Toast) Notifications'
             checked: viewstate.showPopUpNotifications
             enabled: viewstate.loggedin
             click: (it) -> action 'showpopupnotifications', it.checked
-        }, {
+        }
+        {
             type: 'checkbox'
             label: 'Convert text to emoji'
             checked: viewstate.convertEmoji
             enabled: viewstate.loggedin
             click: (it) -> action 'convertemoji', it.checked
-        }, {
+        }
+        {
             label: 'Color Scheme'
             submenu: [
               {
@@ -186,24 +144,28 @@
                   type: 'radio'
                   checked: viewstate.colorScheme == 'default'
                   click: -> action 'changetheme', 'default'
-              }, {
+              }
+              {
                   label: 'Blue'
                   type: 'radio'
                   checked: viewstate.colorScheme == 'blue'
                   click: -> action 'changetheme', 'blue'
-              }, {
+              }
+              {
                   label: 'Dark'
                   type: 'radio'
                   checked: viewstate.colorScheme == 'dark'
                   click: -> action 'changetheme', 'dark'
-              }, {
+              }
+              {
                   label: 'Material'
                   type: 'radio'
                   checked: viewstate.colorScheme == 'material'
                   click: -> action 'changetheme', 'material'
               }
             ]
-        }, {
+        }
+        {
             label: 'Font Size'
             submenu: [
               {
@@ -211,58 +173,51 @@
                   type: 'radio'
                   checked: viewstate.fontSize == 'x-small'
                   click: -> action 'changefontsize', 'x-small'
-              }, {
+              }
+              {
                   label: 'Small'
                   type: 'radio'
                   checked: viewstate.fontSize == 'small'
                   click: -> action 'changefontsize', 'small'
-              }, {
+              }
+              {
                   label: 'Medium'
                   type: 'radio'
                   checked: viewstate.fontSize == 'medium'
                   click: -> action 'changefontsize', 'medium'
-              }, {
+              }
+              {
                   label: 'Large'
                   type: 'radio'
                   checked: viewstate.fontSize == 'large'
                   click: -> action 'changefontsize', 'large'
-              }, {
+              }
+              {
                   label: 'Extra Large'
                   type: 'radio'
                   checked: viewstate.fontSize == 'x-large'
                   click: -> action 'changefontsize', 'x-large'
               }
             ]
-        }, {
-            label: 'Toggle Full Screen',
-            accelerator: getAccelerator('togglefullscreen'),
-            click: -> action 'togglefullscreen'
-        }, {
-            # seee https://github.com/atom/electron/issues/1507
-            label: 'Zoom In',
-            accelerator: getAccelerator('zoomin'),
-            click: -> action 'zoom', +0.25
-        }, {
-            label: 'Zoom Out',
-            accelerator: getAccelerator('zoomout'),
-            click: -> action 'zoom', -0.25
-        }, {
-            label: 'Reset Zoom',
-            accelerator: getAccelerator('resetzoom'),
-            click: -> action 'zoom'
-        }, {
-            type: 'separator'
-        }, {
+        }
+        { role: 'togglefullscreen' }
+        { role: 'zoomin' }
+        { role: 'zoomout' }
+        { role: 'resetzoom' }
+        { type: 'separator' }
+        {
             label: 'Previous Conversation',
             accelerator: getAccelerator('previousconversation')
             enabled: viewstate.loggedin
             click: -> action 'selectNextConv', -1
-        }, {
+        }
+        {
             label: 'Next Conversation',
             accelerator: getAccelerator('nextconversation')
             enabled: viewstate.loggedin
             click: -> action 'selectNextConv', +1
-        }, {
+        }
+        {
             label: 'Select Conversation',
             enabled: viewstate.loggedin
             submenu: [
@@ -270,73 +225,77 @@
                   label: 'Conversation 1'
                   accelerator: getAccelerator('conversation1')
                   click: -> action 'selectConvIndex', 0
-              }, {
+              }
+              {
                   label: 'Conversation 2'
                   accelerator: getAccelerator('conversation2')
                   click: -> action 'selectConvIndex', 1
-              }, {
+              }
+              {
                   label: 'Conversation 3'
                   accelerator: getAccelerator('conversation3')
                   click: -> action 'selectConvIndex', 2
-              }, {
+              }
+              {
                   label: 'Conversation 4'
                   accelerator: getAccelerator('conversation4')
                   click: -> action 'selectConvIndex', 3
-              }, {
+              }
+              {
                   label: 'Conversation 5'
                   accelerator: getAccelerator('conversation5')
                   click: -> action 'selectConvIndex', 4
-              }, {
+              }
+              {
                   label: 'Conversation 6'
                   accelerator: getAccelerator('conversation6')
                   click: -> action 'selectConvIndex', 5
-              }, {
+              }
+              {
                   label: 'Conversation 7'
                   accelerator: getAccelerator('conversation7')
                   click: -> action 'selectConvIndex', 6
-              }, {
+              }
+              {
                   label: 'Conversation 8'
                   accelerator: getAccelerator('conversation8')
                   click: -> action 'selectConvIndex', 7
-              }, {
+              }
+              {
                   label: 'Conversation 9'
                   accelerator: getAccelerator('conversation9')
                   click: -> action 'selectConvIndex', 8
               }
             ]
-        }, {
-            type: 'separator'
-        }, {
+        }
+        { type: 'separator' }
+        {
             label: 'Show tray icon'
             type: 'checkbox'
             enabled: not viewstate.hidedockicon
             checked:  viewstate.showtray
             click: -> action 'toggleshowtray'
         }
-        if isDarwin
-            {
-                label: 'Hide Dock icon'
-                type: 'checkbox'
-                enabled: viewstate.showtray
-                checked:  viewstate.hidedockicon
-                click: -> action 'togglehidedockicon'
-            }
+        {
+            label: 'Hide Dock icon'
+            type: 'checkbox'
+            enabled: viewstate.showtray
+            checked:  viewstate.hidedockicon
+            click: -> action 'togglehidedockicon'
+        } if isDarwin
     ].filter (n) -> n != undefined

 templateWindow = (viewstate) -> [
+    { role: 'minimize' }
     {
-        label: 'Minimize'
-        accelerator: getAccelerator('minimize')
-        selector: 'performMiniaturize:' if isDarwin
-    }, {
         label: 'Close'
         accelerator: getAccelerator('close')
-        selector: 'performClose:' if isDarwin
-    }, {
-        type: 'separator'
-    }, {
+        role: 'close'
+    }
+    { type: 'separator' }
+    {
         label: 'Bring All to Front',
-        selector: 'arrangeInFront:' if isDarwin
+        role: 'front'
     }
 ]

@@ -358,11 +317,10 @@
             label: 'View'
             submenu: templateView viewstate
         }
-        if isDarwin
-            {
-                label: 'Window'
-                submenu: templateWindow viewstate
-            }
+        {
+            label: 'Window'
+            submenu: templateWindow viewstate
+        } if isDarwin
     ].filter (n) -> n != undefined

 module.exports = (viewstate) ->


use _.find with ?

use _.find with ?

--- tutor/src/helpers/plan.coffee
+++ tutor/src/helpers/plan.coffee
@@ -29,10 +29,7 @@

   isPlanOpen: (plan) ->
     now = moment(TimeStore.getNow())
-    for tasking in plan.tasking_plans
-      if moment(tasking.opens_at).isBefore(now)
-        return true # at least one tasking is opened
-    false
+    !! _.find plan?.tasking_plans, (tasking) -> moment(tasking.opens_at).isBefore(now)

 module.exports = PlanHelper


Add startMode option to start with year or month selection.

--- app/assets/javascripts/anjlab/datepicker.js.coffee
+++ app/assets/javascripts/anjlab/datepicker.js.coffee
@@ -211,7 +211,8 @@
       else
         @element.on 'click', $.proxy(@show, this)

-    @viewMode = 0;
+    @startMode = options.startMode || 0
+    @viewMode = @startMode
     @weekStart = options.weekStart || @element.data('date-weekstart') || Locales[@locale].dates.weekStart
     @weekEnd = if this.weekStart == 0 then 6 else @weekStart - 1
     @fillDow()
@@ -237,7 +238,7 @@
   hide: ->
     @picker.hide()
     $(window).off 'resize', @place
-    @viewMode = 0
+    @viewMode = @startMode
     @showMode()
     $(document).off('mousedown', @hide) if !@isInput


Update frontend_application.coffee

Update frontend_application.coffee

--- app/assets/javascripts/frontend_application.coffee
+++ app/assets/javascripts/frontend_application.coffee
@@ -1,11 +1,13 @@
 #= require bxslider/jquery.bxslider.min
 # require_self

-$(document).on 'ready turbolinks:load', ->
-
+ready = ->
   $('.bxslider').show().bxSlider
     auto: true
     slideWidth: 1010
     autoControls: true
     pause: 2000
   return
+
+$(document).ready(ready)
+$(document).on('page:load', ready)


undid log

undid log

--- Framework/Network.coffee
+++ Framework/Network.coffee
@@ -41,9 +41,7 @@
     else
       @xhr.send(options.params)

-  reset: ->
-    Ti.API.info("xhr reset")
-    @xhr.abort()
+  reset: -> @xhr.abort()

   onError: (options) =>
     options.try++


Isolated some logic

Isolated some logic

--- moon-phase.widget/index.coffee
+++ moon-phase.widget/index.coffee
@@ -177,16 +177,12 @@
   moon_age = days_passed % synodic_month
   moon_age = (moon_age).toFixed(1)

-  # handle leading zeroes
-  fracillum = Number(data.illum)
-
-  # if moon phase isn't modulo 25, curphase is provided
-  if data.curphase?
-    curphase = data.curphase
-  else
-    curphase = data.closestphase.phase
-
   if @option.showPhase
+    # if moon phase isn't modulo 25, curphase is provided
+    if data.curphase?
+      curphase = data.curphase
+    else
+      curphase = data.closestphase.phase
     moonEl.find('.phase').text "#{curphase}"

 # ###FIXME
@@ -204,6 +200,8 @@
     moonEl.find('.age').text "#{moon_age} days old"

   if @option.showIllum
+    # handle leading zeroes
+    fracillum = Number(data.illum)
     moonEl.find('.illum').text "#{fracillum}% illumination"

   if @option.showCoords
@@ -241,16 +239,16 @@
         my_time = d.time
       time.append "<div>#{my_time}</div>"

-  # closest phase looks both backwards and fowards: determine which
-  clday = new Date(data.closestphase.date + " " + data.closestphase.time)
-
-  # compare in milliseconds to avoid string issues
-  if clday.getTime() > today.getTime()
-    clause = 'is'
-  else
-    clause = 'was'
-
   if @option.showClosest
+    # closest phase looks both backwards and fowards: determine which
+    clday = new Date(data.closestphase.date + " " + data.closestphase.time)
+
+    # compare in milliseconds to avoid string issues
+    if clday.getTime() > today.getTime()
+      clause = 'is'
+    else
+      clause = 'was'
+
     moonEl.find('.clphase').text "Closest phase #{clause} #{data.closestphase.phase}"

     if @option.showCloseDay


Attempting better moon accuracy

Attempting better moon accuracy

--- moon-phase.widget/index.coffee
+++ moon-phase.widget/index.coffee
@@ -180,13 +180,14 @@

   if data.curphase?
     curphase = data.curphase
-    # when key phase names appear, ensure matching icons
-    if curphase is "New Moon" then      icon_age = 0
-    if curphase is "First Quarter" then icon_age = 7
-    if curphase is "Full Moon" then     icon_age = 14
-    if curphase is "Last Quarter" then  icon_age = 21
   else
     curphase = data.closestphase.phase
+
+  # when key phase names appear, ensure matching icons
+  if curphase is "New Moon" then      icon_age = 0
+  if curphase is "First Quarter" then icon_age = 7
+  if curphase is "Full Moon" then     icon_age = 14
+  if curphase is "Last Quarter" then  icon_age = 21

   moonEl.find('.icon').html @getIcon(icon_age, @option.iconSet)
   moon_age = (moon_age).toFixed(1)


comment out logging messages in JS

comment out logging messages in JS

--- app/assets/javascripts/hat-trick.js.coffee
+++ app/assets/javascripts/hat-trick.js.coffee
@@ -28,7 +28,7 @@

   setAction: (url, method) ->
     methodLower = method.toLowerCase()
-    console.log "Setting form action to #{methodLower} #{url}"
+    # console.log "Setting form action to #{methodLower} #{url}"
     @form.attr("action", url)
     @form.attr("method", "post")
     @form.formwizard("option", remoteAjax: this.ajaxEvents())
@@ -62,9 +62,10 @@
       url: @form.attr("action"),
       dataType: "json",
       beforeSubmit: (data) =>
-        console.log "Sending these data to the server: #{JSON.stringify(data)}"
+        # console.log "Sending these data to the server: #{JSON.stringify(data)}"
+        true
       success: (response) =>
-        console.log "Successful form POST; got #{JSON.stringify(response)}"
+        # console.log "Successful form POST; got #{JSON.stringify(response)}"
         if response.metadata?
           this.setAction(response.metadata.url, response.metadata.method)
         $.extend(window.hatTrick, response) # merge new data with hatTrick
@@ -104,7 +105,7 @@

   repeatStep: (step) ->
     $sourceStep = this.findStep(step.repeatOf.fieldset)
-    console.log "Cloning repeated step #{step.repeatOf.fieldset}"
+    # console.log "Cloning repeated step #{step.repeatOf.fieldset}"
     $clonedStep = $sourceStep.clone(true, true)
     $clonedStep.css("display", "none")
     $clonedStep.attr("id", step.name)
@@ -134,7 +135,8 @@
       remoteAjax: this.ajaxEvents(),
       formOptions:
         success: (data) =>
-          console.log "Successful form POST"
+          # console.log "Successful form POST"
+          true
         beforeSubmit: (data) =>
           console.log "Sending these data to the server: #{JSON.stringify(data)}"

@@ -153,7 +155,7 @@
   setCurrentStepField: ->
     stepId = this.currentStepId()
     this.setHTMeta("step", stepId)
-    console.log "Current form step: #{stepId}"
+    # console.log "Current form step: #{stepId}"

   clearNextStepField: ->
     this.clearHTMeta("next_step")
@@ -188,6 +190,7 @@
       $radio.find("[value=\"#{value}\"]").attr("checked", "checked")

   setFormFields: (model) ->
+    # console.log "Setting form fields based on: #{JSON.stringify(model)}"
     this.fillTextFields(model)
     this.setSelectFields(model)
     this.setCheckboxes(model)
@@ -200,10 +203,10 @@
     $buttonsDiv = this.currentStep().find("div.buttons")
     switch name
       when "next"
-        console.log "Setting submit button val to #{label}"
+        # console.log "Setting submit button val to #{label}"
         $buttonsDiv.find('input:submit').val(label)
       when "back"
-        console.log "Setting reset button val to #{label}"
+        # console.log "Setting reset button val to #{label}"
         $buttonsDiv.find('input:reset').val(label)
       else
         buttonSelector = """input:button[name="#{name}"][value="#{label}"]"""
@@ -253,7 +256,7 @@
   $form = $("form.wizard")
   window.hatTrick = {} unless window.hatTrick?
   unless window.hatTrick.wizard?
-    console.log "Creating new HatTrickWizard instance"
+    # console.log "Creating new HatTrickWizard instance"
     window.hatTrick.wizard = new HatTrickWizard($form, hatTrick.metadata)

 camelizeString = (string) ->


set error messages from ajax reqs in ul.hat_trick_errors

--- app/assets/javascripts/hat-trick.js.coffee
+++ app/assets/javascripts/hat-trick.js.coffee
@@ -68,7 +68,16 @@
           this.setAction(data.wizardMetadata.url, data.wizardMetadata.method)
         # merge new data with window.htData
         $.extend(window.htData, data)
+      error: (data) =>
+        appErrors = eval "(#{data.responseText})"
+        this.addErrorItem value[0] for key, value of appErrors.formModel
     ajax
+
+  addErrorItem: (message) ->
+    $errorList = this.currentStep().find("ul.hat_trick_errors")
+    if $errorList.length > 0
+      $errorList.append("<li>#{message}</li>")
+      $errorList.show()

   updateSteps: ->
     @form.formwizard("update_steps")


auth: add `notification` serviceType

auth: add `notification` serviceType

- authOk => auth.authOk

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -8,7 +8,14 @@
     type        : 'fanout'
     autoDelete  : yes

-  constructor:(@bongo, @resourceName, @presenceExchange='services-presence')->
+  NOTIFICATION_EXCHANGE_OPTIONS =
+    type        : 'direct'
+    autoDelete  : yes
+
+  constructor:(@bongo, options = {})->
+    { @presenceExchange, @notificationExchange } = options
+    @presenceExchange     ?= 'services-presence'
+    @notificationExchange ?= 'notification-control'
     @services = {}
     @clients  = {
       bySocketId    : {}
@@ -77,7 +84,7 @@
   addClient:(socketId, exchange, routingKey, sendOk=yes)->
     if sendOk
       @bongo.respondToClient routingKey, {
-        method    : 'authOk'
+        method    : 'auth.authOk'
         arguments : []
         callbacks : {}
       }
@@ -90,12 +97,20 @@
     clientsByExchange.push client

   rejectClient:(routingKey, message)->
+    console.log 'rejecting', routingKey
     return console.trace()  unless routingKey?
     @bongo.respondToClient routingKey, {
       method    : 'error'
       arguments : [message: message ? 'Access denied']
       callbacks : {}
     }
+
+  fetchNotificationExchange:(callback)->
+    @bongo.mq.connection.exchange(
+      @notificationExchange
+      NOTIFICATION_EXCHANGE_OPTIONS
+      callback
+    )

   joinClient: do ->

@@ -155,21 +170,42 @@
                           message = JSON.stringify secretChannelName
                           @bongo.respondToClient setSecretNameEvent, message

+    joinClientNotificationHelper =(messageData, routingKey, socketId)->
+      fail = (err)=>
+        console.error err  if err
+        @rejectClient routingKey
+
+      @authenticate messageData, routingKey, (session)=>
+        unless session then fail()
+        else if session?
+          {username} = session
+          @fetchNotificationExchange (exchange)->
+            exchange.publish 'auth.join', {routingKey, username}
+
     joinClient =(messageData, socketId)->
       {channel, routingKey, serviceType} = messageData
       switch serviceType
         when 'bongo', 'kite'
           joinClientHelper.call this, messageData, routingKey, socketId
+
         when 'group'
-          unless ///^group.#{messageData.group}///.test routingKey
-            console.log 'rejecting', routingKey
+          unless ///^group\.#{messageData.group}\.///.test routingKey
             return @rejectClient routingKey
           joinClientGroupHelper.call this, messageData, routingKey, socketId
+
         when 'chat' then # TODO: handle authentication for chat
+
+        when 'notification'
+          unless ///^notification\.///.test routingKey
+            return @rejectClient routingKey
+          joinClientNotificationHelper.call this, messageData, routingKey, socketId
+
         when 'secret'
           @addClient socketId, routingKey, routingKey, no
+
         else
-          @rejectClient routingKey
+          @rejectClient routingKey  unless /^oid./.test routingKey
+          # TODO: we're not really handling the oid channels at all (I guess we don't need to) C.T.

   cleanUpClient:(client)->
     @removeClient client


auth worker: wait for bongo to be completely ready before consuming messages

--- workers/auth/lib/auth/authworker.coffee
+++ workers/auth/lib/auth/authworker.coffee
@@ -532,7 +532,7 @@

   connect: ->
     {bongo} = this
-    bongo.mq.ready =>
+    bongo.on 'connected', =>
       {connection} = bongo.mq
       @monitorPresence connection


Enable last visual result in keyboard selection for QuickJump.Dropdown

--- app/assets/javascripts/quickjump.js.coffee
+++ app/assets/javascripts/quickjump.js.coffee
@@ -133,12 +133,10 @@
     out = this.renderResultsToString(query)
     if out != ""
       out += '

'
-    # if query == ""
     if query == ""
-      out += ('
'+@parent.index_link+'
').replace(/%\{type\}/g, @parent.type).replace(/%\{query\}/g, query)
+      out += ('
'+@parent.index_link+'
').replace(/%\{type\}/g, @parent.type).replace(/%\{query\}/g, query)
     else
-      out += ('
 '+@parent.new_template+'
').replace(/%\{type\}/g, @parent.type).replace(/%\{query\}/g, query)
-    #  out += '

'
+      out += ('
 '+@parent.new_template+'
').replace(/%\{type\}/g, @parent.type).replace(/%\{query\}/g, query)

     last_li = $(@selector).find("li.insert-results-after")
     last_li.nextAll().remove()
@@ -168,8 +166,13 @@
     anchor or= this.getActiveResult()
     if anchor?
       index = $(anchor).data("result-index")
-      result = @results[index]
-      @result_callback.apply(null, [result])
+      console.log( anchor, index )
+      if index?
+        result = @results[index]
+        @result_callback.apply(null, [result])
+      else
+        # follow link
+        window.location.href = $(anchor).find("a").attr("href")

   getActiveResult: ->
     if @active_result?
@@ -181,6 +184,9 @@

   deactivate: ->

+  getMaxResult: ->
+    Math.min(@results.length, @MAX_RESULTS)
+
   hide: ->
     this.deactivate()

@@ -190,8 +196,7 @@
       this.getActiveResult().addClass "active"

   moveSelection: (modifier) ->
-    console.log "moveSelection", @active_result, @results.length, @MAX_RESULTS
-    max_result = Math.min(@results.length, @MAX_RESULTS)
+    max_result = this.getMaxResult()
     @active_result += modifier
     @active_result = max_result-1 if @active_result < 0
     @active_result = 0 if @active_result > max_result-1
@@ -266,6 +271,7 @@

 class OUT.QuickJump.Dropdown extends OUT.QuickJump.Base
+  MAX_RESULTS: 20
   constructor: (@dropdown) ->
     @data_url = $(@dropdown).data("target-url")
     @type = $(@dropdown).data("target-type")
@@ -295,6 +301,9 @@
       title: @search_link
     results

+  getMaxResult: ->
+    Math.min(@results.length, @MAX_RESULTS) + 1
+

 OUT.lazyTimerIds = {}
 OUT.setLazyTimer = (name, delay, func) ->


Add a helper method to add converters without directly manipulating the hash.

--- app/assets/javascripts/slickbone.js.coffee
+++ app/assets/javascripts/slickbone.js.coffee
@@ -69,15 +69,15 @@
     # that the Backbone.js model is all set up.
     @bind 'change', => @_executeDerivations()

+  setupAssociations:  ->
+  setupConverters:    ->
+  setupDerivations:   ->
+
   # Helper method used by the association definition methods. Actually modifies the
   # assocation definition hash.
   _addAssociation: (name, model, type) ->
     @associations[name] = { model: model, associationType: type }

-  setupAssociations:  ->
-  setupConverters:    ->
-  setupDerivations:   ->
-
   # indicates that a given attribute is actually a Collection. Adds the correct settings to
   # the association hash.
   hasMany:   (name, collection) -> @_addAssociation(name, collection, 'hasMany')
@@ -105,7 +105,10 @@
     @derivations.unshift
       field: fieldName
       func: derivationFunction
-
+
+  addConverter: (fieldName, conversionFunction) ->
+    @converters[fieldName] = conversionFunction
+
   # Override the default Backbone.js set method to handle our added functionality.
   # Iterate through the attributes being set and:
   # * If the attribute name is one of the associations, set that as the attribute instead of
@@ -123,7 +126,7 @@
         else
           throw "The conversion function for #{attribute} is invalid; it must be a function."
     super
-
+
   # Override the default Backbone.js `toJSON` method to handle associations.
   toJSON: ->
     result = super


Cleaned up user_sync

Cleaned up user_sync

--- app/assets/javascripts/user_sync.js.coffee
+++ app/assets/javascripts/user_sync.js.coffee
@@ -1,17 +1,31 @@
 class UserSync

   constructor: (selector) ->
-    @collapseButtons = $("#{selector} .collapse-btn")
-    @panels = $("#{selector} .panel")
+    @ui =
+      collapseButtons: $("#{selector} .collapse-btn")
+      panels: $("#{selector} .panel")
+
     @collapsePanelsIfNeededAtStart()
+    @bindToEvents()

-    @collapseButtons.click((ev) =>
+  bindToEvents: ->
+    @ui.collapseButtons.click((ev) =>
       button = $(ev.target)
       panel = $(ev.target).parent().parent().parent().find('.panel')
       collapse = button.hasClass('fa-caret-up')
-      @replaceButton(button, collapse)
-      @collapseOrNot(panel, collapse)
+      @collapse(panel, collapse)
     )
+
+  collapsePanelsIfNeededAtStart: ->
+    @ui.panels.each (index, el) =>
+      if $.cookie(@cookieNameForIndex(index))
+        $(el).addClass('no-transition')
+        @collapse($(el), true)
+
+  collapse: (panel, collapse) ->
+    button = $(panel).parent().find('.collapse-btn')
+    @replaceButton(button, collapse)
+    @modifyPanel(panel, collapse)

   replaceButton: (btn, collapse) ->
     if collapse
@@ -19,9 +33,9 @@
     else
       btn.removeClass('fa-caret-down').addClass('fa-caret-up')

-  collapseOrNot: (panel, collapse) ->
+  modifyPanel: (panel, collapse) ->
     index = $('.panel.project-list').index(panel)
-    cookie = "panel-collapsed-#{index}"
+    cookie = @cookieNameForIndex(index)
     if collapse
       panel.addClass('collapsed')
       $.cookie(cookie, true, {expires: 1})
@@ -29,15 +43,8 @@
       panel.removeClass('collapsed').removeClass('no-transition')
       $.removeCookie(cookie)

-  collapsePanelsIfNeededAtStart: ->
-    @panels.each((index, el) =>
-      cookie = "panel-collapsed-#{index}"
-      if $.cookie(cookie)
-        $(el).addClass('no-transition')
-        @collapseOrNot($(el), true)
-        @replaceButton($(el).parent().find('.fa-caret-up'), true)
-    )
-
+  cookieNameForIndex: (index) ->
+    "panel-collapsed-#{index}"

 $(document).on 'ready page:load', ->
   new UserSync('.user-sync')


set default milestone param for filters

set default milestone param for filters

--- app/coffee/modules/controllerMixins.coffee
+++ app/coffee/modules/controllerMixins.coffee
@@ -244,6 +244,7 @@
             loadFilters[key] = urlfilters[key]
             loadFilters[excludeKey] = urlfilters[excludeKey]

+        loadFilters.milestone = "null"
         if milestone
             loadFilters.milestone = milestone


adapt US filter for work with include/exclude modes

--- app/coffee/modules/controllerMixins.coffee
+++ app/coffee/modules/controllerMixins.coffee
@@ -172,18 +172,29 @@
 #############################################################################

 class UsFiltersMixin
+    excludePrefix: "exclude_"
+    filterCategories: [
+        "tags",
+        "status",
+        "assigned_users",
+        "assigned_to",
+        "owner",
+        "epic",
+        "role",
+    ]
+
     changeQ: (q) ->
         @.replaceFilter("q", q)
         @.filtersReloadContent()
         @.generateFilters()

     removeFilter: (filter) ->
-        @.unselectFilter(filter.dataType, filter.id)
+        @.unselectFilter(filter.dataType, filter.id, false, filter.mode)
         @.filtersReloadContent()
         @.generateFilters()

     addFilter: (newFilter) ->
-        @.selectFilter(newFilter.category.dataType, newFilter.filter.id)
+        @.selectFilter(newFilter.category.dataType, newFilter.filter.id, false, newFilter.mode)
         @.filtersReloadContent()
         @.generateFilters()

@@ -195,13 +206,11 @@
     saveCustomFilter: (name) ->
         filters = {}
         urlfilters = @location.search()
-        filters.tags = urlfilters.tags
-        filters.status = urlfilters.status
-        filters.assigned_to = urlfilters.assigned_to
-        filters.assigned_users = urlfilters.assigned_users
-        filters.owner = urlfilters.owner
-        filters.epic = urlfilters.epic
-        filters.role = urlfilters.role
+
+        for key in @.filterCategories
+            excludeKey = @.excludePrefix.concat(key)
+            filters[key] = urlfilters[key]
+            filters[excludeKey] = urlfilters[excludeKey]

         @filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) =>
             userFilters[name] = filters
@@ -228,14 +237,12 @@

         loadFilters = {}
         loadFilters.project = @scope.projectId
-        loadFilters.tags = urlfilters.tags
-        loadFilters.status = urlfilters.status
-        loadFilters.assigned_users = urlfilters.assigned_users
-        loadFilters.assigned_to = urlfilters.assigned_to
-        loadFilters.owner = urlfilters.owner
-        loadFilters.epic = urlfilters.epic
-        loadFilters.role = urlfilters.role
         loadFilters.q = urlfilters.q
+
+        for key in @.filterCategories
+            excludeKey = @.excludePrefix.concat(key)
+            loadFilters[key] = urlfilters[key]
+            loadFilters[excludeKey] = urlfilters[excludeKey]

         if milestone
             loadFilters.milestone = milestone
@@ -246,18 +253,19 @@
         ]).then (result) =>
             data = result[0]
             customFiltersRaw = result[1]
-
-            statuses = _.map data.statuses, (it) ->
+            dataCollection = {}
+
+            dataCollection.status = _.map data.statuses, (it) ->
                 it.id = it.id.toString()

                 return it
-            tags = _.map data.tags, (it) ->
+            dataCollection.tags = _.map data.tags, (it) ->
                 it.id = it.name

                 return it
-            tagsWithAtLeastOneElement = _.filter tags, (tag) ->
+            tagsWithAtLeastOneElement = _.filter dataCollection.tags, (tag) ->
                 return tag.count > 0
-            assignedUsers = _.map data.assigned_users, (it) ->
+            dataCollection.assigned_users = _.map data.assigned_users, (it) ->
                 if it.id
                     it.id = it.id.toString()
                 else
@@ -266,7 +274,7 @@
                 it.name = it.full_name || "Unassigned"

                 return it
-            assignedTo = _.map data.assigned_to, (it) ->
+            dataCollection.assigned_to = _.map data.assigned_to, (it) ->
                 if it.id
                     it.id = it.id.toString()
                 else
@@ -275,7 +283,7 @@
                 it.name = it.full_name || "Unassigned"

                 return it
-            role = _.map data.roles, (it) ->
+            dataCollection.role = _.map data.roles, (it) ->
                 if it.id
                     it.id = it.id.toString()
                 else
@@ -284,12 +292,12 @@
                 it.name = it.name || "Unassigned"

                 return it
-            owner = _.map data.owners, (it) ->
+            dataCollection.owner = _.map data.owners, (it) ->
                 it.id = it.id.toString()
                 it.name = it.full_name

                 return it
-            epic = _.map data.epics, (it) ->
+            dataCollection.epic = _.map data.epics, (it) ->
                 if it.id
                     it.id = it.id.toString()
                     it.name = "##{it.ref} #{it.subject}"
@@ -301,33 +309,14 @@

             @.selectedFilters = []

-            if loadFilters.status
-                selected = @.formatSelectedFilters("status", statuses, loadFilters.status)
-                @.selectedFilters = @.selectedFilters.concat(selected)
-
-            if loadFilters.tags
-                selected = @.formatSelectedFilters("tags", tags, loadFilters.tags)
-                @.selectedFilters = @.selectedFilters.concat(selected)
-
-            if loadFilters.assigned_users
-                selected = @.formatSelectedFilters("assigned_users", assignedUsers, loadFilters.assigned_users)
-                @.selectedFilters = @.selectedFilters.concat(selected)
-
-            if loadFilters.assigned_to
-                selected = @.formatSelectedFilters("assigned_to", assignedTo, loadFilters.assigned_to)
-                @.selectedFilters = @.selectedFilters.concat(selected)
-
-            if loadFilters.owner
-                selected = @.formatSelectedFilters("owner", owner, loadFilters.owner)
-                @.selectedFilters = @.selectedFilters.concat(selected)
-
-            if loadFilters.epic
-                selected = @.formatSelectedFilters("epic", epic, loadFilters.epic)
-                @.selectedFilters = @.selectedFilters.concat(selected)
-
-            if loadFilters.role
-                selected = @.formatSelectedFilters("role", role, loadFilters.role)
-                @.selectedFilters = @.selectedFilters.concat(selected)
+            for key in @.filterCategories
+                excludeKey = @.excludePrefix.concat(key)
+                if loadFilters[key]
+                    selected = @.formatSelectedFilters(key, dataCollection[key], loadFilters[key])
+                    @.selectedFilters = @.selectedFilters.concat(selected)
+                if loadFilters[excludeKey]
+                    selected = @.formatSelectedFilters(key, dataCollection[key], loadFilters[excludeKey], "exclude")
+                    @.selectedFilters = @.selectedFilters.concat(selected)

             @.filterQ = loadFilters.q

@@ -335,34 +324,34 @@
                 {
                     title: @translate.instant("COMMON.FILTERS.CATEGORIES.STATUS"),
                     dataType: "status",
-                    content: statuses
+                    content: dataCollection.status
                 },
                 {
                     title: @translate.instant("COMMON.FILTERS.CATEGORIES.TAGS"),
                     dataType: "tags",
-                    content: tags,
+                    content: dataCollection.tags,
                     hideEmpty: true,
                     totalTaggedElements: tagsWithAtLeastOneElement.length
                 },
                 {
                     title: @translate.instant("COMMON.FILTERS.CATEGORIES.ASSIGNED_USERS"),
                     dataType: "assigned_users",
-                    content: assignedUsers
+                    content: dataCollection.assigned_users
                 },
                 {
                     title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
                     dataType: "role",
-                    content: role
+                    content: dataCollection.role
                 },
                 {
                     title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
                     dataType: "owner",
-                    content: owner
+                    content: dataCollection.owner
                 },
                 {
                     title: @translate.instant("COMMON.FILTERS.CATEGORIES.EPIC"),
                     dataType: "epic",
-                    content: epic
+                    content: dataCollection.epic
                 }
             ]


don't treat calendar events as external draggables

fixes CNVS-24607

test-plan:
 - calendar drag and drop works for moving/resizing dated events
 - dragging/resizing dated events doesn't experience visual flicker
 - dragging undated events onto the calendar works

Change-Id: Ie5a4377a599b9ed8416fb47fe6f791fda41a2f60
Reviewed-on: https://gerrit.instructure.com/67164
Reviewed-by: Jonathan Featherstone <c99460343437e667b0585ea68fd7caf7bd0a02ab@instructure.com>
Product-Review: Jacob Fugal <f862f167b85d41b225785c70d70808bc7337c1fe@instructure.com>
Tested-by: Jenkins
QA-Review: August Thornton <9adc7a1161ddf32ff608de792a7e50179545f026@instructure.com>

--- app/coffeescripts/calendar/Calendar.coffee
+++ app/coffeescripts/calendar/Calendar.coffee
@@ -142,7 +142,7 @@
         slotDuration: '00:30:00'
         scrollTime: '07:00:00'
         droppable: true
-        dropAccept: '.fc-event,.undated_event'
+        dropAccept: '.undated_event'
         events: @getEvents
         eventRender: @eventRender
         eventAfterRender: @eventAfterRender


fix repository full_name

fix repository full_name

--- scripts/befool.blog.coffee
+++ scripts/befool.blog.coffee
@@ -21,7 +21,7 @@
     # listen to webhook
     robot.router.post '/bii/befool/publish', (req, res) ->
         ref = req.body.ref
-        repos = req.repository.full_name
+        repos = req.body.repository.full_name
         if ref.split('/').pop() isnt config.UPDATE_BRANCH
             return res.send 'OK, but I\'m not interested about that. ^ ^;'
         robot.logger.info "Befool blog updated"


Fix exception handling

Fix exception handling

--- client/app/initialize.coffee
+++ client/app/initialize.coffee
@@ -9,8 +9,8 @@
                 type: 'error'
                 error:
                     msg: msg
-                    full: error.toString()
-                    stack: error.stack
+                    full: exception
+                    stack: error?.stack
                 url: url
                 line: line
                 col: col


Minor whitespacing

Minor whitespacing

--- app/js/jail_iframe/initializers/xdm.coffee
+++ app/js/jail_iframe/initializers/xdm.coffee
@@ -4,7 +4,6 @@
 Factlink.listenToWindowMessages null, Factlink.modal
 iframe.src = "#{FactlinkConfig.api}/factlink/intermediate"

-
 Factlink.remote = Factlink.createFrameProxyObject iframe.contentWindow, ['showFactlink', 'prepareNewFactlink' ]


removed duplicated error message

removed duplicated error message

--- client/views/app/room.coffee
+++ client/views/app/room.coffee
@@ -534,9 +534,7 @@
 			newFile = new (FS.File)(file)
 			newFile.rid = Session.get('openedRoom')
 			Files.insert newFile, (error, fileObj) ->
-				if error
-					toastr.error 'Upload failed... please try again. Error: ' + error
-				else
+				unless error
 					toastr.success 'Upload succeeded!'
 					console.log('room fileObj', fileObj) if window.rocketDebug
 					Meteor.call 'sendMessage',


game state

game state

--- src/game_state_single_camera.coffee
+++ src/game_state_single_camera.coffee
@@ -6,12 +6,13 @@
 @fieldOf GameState
 @module
 @param {Object} I Instance variables
-@param {Object} self Reference to the engine
+@param {Object} self Reference to the game state
 ###
 GameState.SingleCamera = (I, self) ->
   # Set some default properties
   Object.reverseMerge I,
     cameraTransform: Matrix.IDENTITY
+    zSort: true

   self.attrAccessor "cameraTransform"


only show pushes to master or develop, show branch in push message, and link to it on GitHub

--- scripts/github-hook.coffee
+++ scripts/github-hook.coffee
@@ -88,13 +88,25 @@
     noun = if size is 1 then singularNoun else singularNoun + "s"
     "#{size} #{noun}"

+  # limit push notifications to master and develop branches
+  refRegex = /// ^                # match from beginning
+    \/ refs                       # string should lead with /refs
+    \/ head                       # then /head
+    \/ (?:                        # non capturing group
+      master | develop            # Then it should show the branch name this pull happened on, we want master or develop
+    )
+  ///
+
   formatPushes = (event) ->

-    {commits, pusher, repository, compare} = event.data
+    {ref, commits, pusher, repository, compare} = event.data
+    return null unless refRegex.match(ref)
     return null unless commits?.length > 0

+    branchName = refs.split('/')[2]
+
     """
-    [#{pusher.name}](https://github.com/#{pusher.name}) pushed #{ordinal(commits.length, 'commit')} to [#{repository.full_name}](https://github.com/#{repository.full_name})
+    [#{pusher.name}](https://github.com/#{pusher.name}) pushed #{ordinal(commits.length, 'commit')} to [#{repository.full_name}/#{branchName}](https://github.com/#{repository.full_name}/tree/#{branchName})

     #{('*' + commit.message.split("\n")[0]) for commit in commits}


Only spec window's web view should force focus

--- src/main-process/atom-window.coffee
+++ src/main-process/atom-window.coffee
@@ -157,8 +157,10 @@

     @setupContextMenu()

-    @browserWindow.on 'blur', =>
-      @browserWindow.focusOnWebView()
+    if @isSpec
+      # Spec window's web view should always have focus
+      @browserWindow.on 'blur', =>
+        @browserWindow.focusOnWebView()

   openPath: (pathToOpen, initialLine, initialColumn) ->
     @openLocations([{pathToOpen, initialLine, initialColumn}])


Fixed coffelint check.

Fixed coffelint check.

--- app/assets/javascripts/app/models/text_module.coffee
+++ app/assets/javascripts/app/models/text_module.coffee
@@ -16,6 +16,7 @@
     'content',
   ]

+  # coffeelint: disable=no_interpolation_in_single_quotes
   @description = '''
 Create Text Modules to **spend less time writing responses**. TextModules can include smart variables like the users name or email address.

@@ -33,4 +34,5 @@
 * @ticket.owner (e. g. @ticket.owner.firstname, @ticket.owner.lastname)
 * @ticket.organization (e. g. @ticket.organization.name)

-'''+'''
+  # coffeelint: enable=no_interpolation_in_single_quotes


Update batch_export.coffee

Update batch_export.coffee

--- app/assets/javascripts/incidents/batch_export.coffee
+++ app/assets/javascripts/incidents/batch_export.coffee
@@ -1,3 +1,13 @@
+alreadyExportedWarning = -> (count)
+  words = []
+  if count == 1
+    words.push 'incident has', 'this incident'
+  else words.push 'incidents have', 'these incidents'
+  """
+  #{count} selected #{words[0]} already been exported.
+  Please doublecheck that you would like to re-export #{words[1]}.
+  """
+
 handleExporting = ->
   inSelectMode = $('table.incidents th.batch-export').is ':visible'
   if inSelectMode
@@ -6,12 +16,9 @@
     .toArray()
     alreadyExported= $('.batch-export input:checked').filter ->
       $(this).data 'exported'
-    if alreadyExported.length > 0
-      if alreadyExported.length == 1
-        warning = '1 selected incident has already been exported.'
-      else warning = "#{alreadyExported.length} selected incidents have already been exported."
-      warning += "\nPlease doublecheck that you would like to re-export this incident."
-      unless confirm warning
+    .length
+    if alreadyExported > 0
+      unless confirm alreadyExportedWarning(alreadyExported)
         return false
      window.location.href = 'incidents/batch_export?' + $.param(ids: ids)
   else