Fix coffee

Fix coffee

--- www/tupelo-main.coffee
+++ www/tupelo-main.coffee
@@ -1,15 +1,14 @@
-### tupelo-main.coffee
+# tupelo-main.coffee
 # vim: sts=2 sw=2 et:
-###

 $ = jQuery
-
+
 $(document).ready ->
   # status object
   tupelo =
     game_state: {}
     events: []
-
+
   states =
     initial:
       show: [ "#register_form" ]
@@ -19,33 +18,36 @@
         reg.addClass "initial"
         reg.val "Your name"
         return
-
+
     registered:
       show: [ "#quit_form", "#games", "#players", "#game_create_form" ]
       hide: [ "#register_form", "#my_game", "#game" ]
       change: ->
-        tupelo.list_timer = setInterval(updateLists, 5000)  if tupelo.list_timer is `undefined`
-
+        if not tupelo.list_timer?
+          tupelo.list_timer = setInterval(updateLists, 5000)
+
     gameCreated:
       show: [ "#my_game" ]
       hide: [ "#game_create_form" ]
-
+
     inGame:
       show: [ "#game" ]
       hide: [ "#games", "#players", "#my_game" ]
-
+
+  # change the current state
   setState = (state, effectDuration) ->
     st = states[state]
     $(elem).hide effectDuration for elem in st.hide
     $(elem).show effectDuration for elem in st.show
     st.change() if st.change?
-
+
   escapeHtml = (str) ->
     str.replace("<", "<").replace ">", ">"
-
+
   dbg = ->
     $("#debug").html JSON.stringify(tupelo) if T.debug
-
+
+  # generic ajax error callback
   ajaxErr = (xhr, astatus, error) ->
     handled = false
     # 403 is for game and rule errors
@@ -59,7 +61,7 @@
       T.log "status: " + astatus
       T.log "error: " + error
     handled
-
+
   hello = ->
     $.ajax
       url: "/ajax/hello"
@@ -70,10 +72,10 @@
           tupelo.player = new T.Player result.player.player_name
           registerOk result.player
           # are we in a game?
-          gameCreateOk result.player.game_id if result.player.game_id?
-
-      error: ajaxErr
-
+          if result.player.game_id?
+            gameCreateOk result.player.game_id
+      error: ajaxErr
+
   updateGameLinks = (disabledIds) ->
     gameJoinClicked = (event) ->
       # "game_join_ID"
@@ -84,39 +86,38 @@
         data:
           akey: tupelo.player.akey
           game_id: game_id
-
         success: gameCreateOk
         error: ajaxErr
-
+
     if tupelo.game_id?
       $("button.game_join").attr "disabled", true
       $("tr#game_id_" + tupelo.game_id).addClass "highlight"
     else
       $("button#" + id).attr "disabled", true for id in disabledIds
       $("button.game_join").click gameJoinClicked
-
+
   listGamesOk = (result) ->
     #T.log "listGamesOk"
     #T.log result
     html = ""
     disabledIds = []
     for own game_id of result
-      html += "
"
+      html += "
"
       #players = []
       #for res in result[game_id]
       #  plr = new T.Player().fromObj res
       #  players.push plr.player_name
       players = (new T.Player().fromObj(res).player_name for res in result[game_id])
-
+
       html += "" + escapeHtml(players.join(", ")) + "	"
       html += "<button class=\"game_join btn\" id=\"game_join_" + game_id + "\">join</button>	"
       html += "
"
-      disabledIds.push "game_join_" + game_id if players.length is 4
-
+      disabledIds.push "game_join_#{game_id}" if players.length is 4
+
     $("#game_list table tbody").html html
     updateGameLinks disabledIds
     dbg()
-
+
   listPlayersOk = (result) ->
     T.log result
     html = ""
@@ -124,12 +125,12 @@
       plr = new T.Player().fromObj(result[player])
       if plr.id isnt tupelo.player.id
         cls = if plr.game_id? then "class=\"in_game\" " else ""
-        html += "
"
+        html += "
"
         html += "" + escapeHtml(plr.player_name) + "
"
-
+
     $("#player_list table tbody").html html
     dbg()
-
+
   registerOk = (result) ->
     $("#name").val ""
     tupelo.player.id = result.id
@@ -143,16 +144,16 @@
     updateLists()
     setState "registered", "fast"
     T.log "timer created"
-
+
   leftGame = ->
     if tupelo.event_fetch_timer?
       tupelo.event_fetch_timer.disable()
       tupelo.event_fetch_timer = null
-
+
     if tupelo.event_timer?
       clearTimeout tupelo.event_timer
       tupelo.event_timer = null
-
+
     tupelo.game_id = null
     tupelo.game_state = {}
     tupelo.hand = null
@@ -176,12 +177,12 @@
     T.log tupelo
     dbg()
     setState "initial", "fast"
-
+
   gameCreateOk = (result) ->
     tupelo.game_id = result
     T.log tupelo
     dbg()
-    $("p#joined_game").html "joined game " + tupelo.game_id
+    $("p#joined_game").html "joined game #{tupelo.game_id}"
     setState "gameCreated", "fast"
     tupelo.event_fetch_timer = new T.Timer("/ajax/get_events", 2000, eventsOk,
       data:
@@ -248,7 +249,7 @@
         statusStr = "NOLO"
       else statusStr = "RAMI" if state.mode is T.RAMI

-    statusStr = "" + statusStr + ""
+    statusStr = "#{statusStr}"
     statusStr += "tricks: " + state.tricks[0] + " - " + state.tricks[1] + ""

     if state.score?
@@ -294,11 +295,11 @@
   updateHand = (newHand) ->
     html = ""
     hand = []
-    for item in newHand
+    for item, i in newHand
       card = new T.Card(item.suit, item.value)
       hand.push card
       html += "" if tupelo.my_turn
-      html += "" + card.toShortHtml() + ""
+      html += "" + card.toShortHtml() + ""
       html += "" if tupelo.my_turn

     tupelo.hand = hand
@@ -361,7 +362,8 @@
       else
         T.log "unknown event " + event.type

-    tupelo.event_timer = setTimeout(processEvent, 0) if handled is true
+    if handled is true
+      tupelo.event_timer = setTimeout(processEvent, 0)

   eventsOk = (result) ->
     ###
@@ -373,7 +375,9 @@
     # push events to queue
     tupelo.events.push event for event in result

-    tupelo.event_timer = setTimeout(processEvent, 0) if tupelo.events.length > 0 and tupelo.event_timer is `undefined`
+    if tupelo.events.length > 0 and not tupelo.event_timer?
+      tupelo.event_timer = setTimeout(processEvent, 0)
+
     dbg()

   gameInfoOk = (result) ->
@@ -425,18 +429,18 @@
       data:
         akey: tupelo.player.akey

-$("#echo_ajax").click ->
-  text = $("#echo").val()
-  $.ajax
-    url: "/ajax/echo"
-    data:
-      test: text
-
-    success: (result) ->
-      $("#echo_result").html escapeHtml(result)
-      $("#echo").val ""
-
-    error: ajaxErr
+  # bind DOM events
+
+  $("#echo_ajax").click ->
+    text = $("#echo").val()
+    $.ajax
+      url: "/ajax/echo"
+      data:
+        test: text
+      success: (result) ->
+        $("#echo_result").html escapeHtml(result)
+        $("#echo").val ""
+      error: ajaxErr

   $("#register_btn").click ->
     input = $("#register_name")
@@ -444,13 +448,13 @@
     if not name or input.hasClass("initial")
       alert "Please enter your name first"
       return
+
     tupelo.player = new T.Player(name)
     T.log tupelo
     $.ajax
       url: "/ajax/player/register"
       data:
         player: JSON.stringify(tupelo.player)
-
       success: registerOk
       error: ajaxErr

@@ -468,10 +472,9 @@
       url: "/ajax/player/quit"
       data:
         akey: tupelo.player.akey
-
       success: quitOk
       error: (xhr, astatus, error) ->
-        quitOk()  if ajaxErr(xhr, astatus, error) is true
+        quitOk() if ajaxErr(xhr, astatus, error) is true

   $(".game_leave_btn").click ->
     # TODO: should we cancel timers already here?
@@ -480,10 +483,9 @@
       data:
         akey: tupelo.player.akey
         game_id: tupelo.game_id
-
       success: leaveOk
       error: (xhr, astatus, error) ->
-        leaveOk()  if ajaxErr(xhr, astatus, error) is true
+        leaveOk() if ajaxErr(xhr, astatus, error) is true

   $("#game_create_btn").click ->
     # TODO: should we cancel timers already here?
@@ -491,7 +493,6 @@
       url: "/ajax/game/create"
       data:
         akey: tupelo.player.akey
-
       success: gameCreateOk
       error: ajaxErr

@@ -501,7 +502,6 @@
       data:
         akey: tupelo.player.akey
         game_id: tupelo.game_id
-
       success: startOk
       error: ajaxErr

@@ -511,7 +511,6 @@
       data:
         akey: tupelo.player.akey
         game_id: tupelo.game_id
-
       success: startOk
       error: ajaxErr

@@ -533,13 +532,14 @@

   # show a confirmation if the browser supports it
   window.onbeforeunload = (e) ->
-    if tupelo.game_id is `undefined`
+    if not tupelo.game_id?
       return `undefined` # no dialog
     e = e or window.event
     msg = "By leaving the page you will also leave the game."
     e.returnValue = msg if e
     msg

+  # and finally, contact the server
   hello()
   setState "initial"


数値を渡すと3桁ごとのカンマ区切りを追加して返すメソッドを追加

数値を渡すと3桁ごとのカンマ区切りを追加して返すメソッドを追加

--- src/coffee/MathUtils.coffee
+++ src/coffee/MathUtils.coffee
@@ -13,4 +13,10 @@
 		if (_digitCount > 0)
 			for i in [0.._digitCount - 1]
 				_string = "0" + _string
-		return _string+		return _string
+
+#	数値を渡すと、3桁ごとのカンマ区切りを追加して返します。
+	@addFigure: (str) ->
+		_num = new String(str).replace(/,/g, "")
+		while(_num != (_num = _num.replace(/^(-?\d+)(\d{3})/, "$1,$2")))
+		return _num


Add missing dependency

Add missing dependency

--- scripts/collections/content.coffee
+++ scripts/collections/content.coffee
@@ -8,11 +8,12 @@
 # New content models should be created by calling `ALL_CONTENT.add {}`

 define [
+  'jquery'
   'underscore'
   'backbone'
   'cs!session'
   'cs!collections/media-types'
-], (_, Backbone, session, mediaTypes) ->
+], ($, _, Backbone, session, mediaTypes) ->

   _loaded = $.Deferred()


fix bug where setcontent and reloadcontent couldn't find dialogs by id

--- vendor/assets/javascripts/nifty/dialog.coffee
+++ vendor/assets/javascripts/nifty/dialog.coffee
@@ -39,6 +39,8 @@
     dialogsOpen = $('div.niftyDialog').length
     dialogID = if dialogsOpen == 0 then this.startingID else (dialogsOpen * 10) + this.startingID

+    options.id = dialogID unless options.id?
+
     # create a template and assign the ID
     dialogTemplate = $("

")
     dialogTemplate.data('dialogID', dialogID)
@@ -131,7 +133,7 @@
   # This method will replace the contents of the nearest dialog (or the one with the
   # given ID if one is given).
   setContent: (content, id = null)->
-    dialog = if id == null then $('div.niftyDialog:last') else $("div.niftyDialog#dialog-#{id}")
+    dialog = if id == null then $('div.niftyDialog:last') else $("div.niftyDialog#niftyDialog-#{id}")
     if dialog.length
       dialog.html(content)
       options = dialog.data('options')
@@ -142,7 +144,7 @@
   # This method will refectch the contents of the nearest dialog (or the one with the
   # given ID if one is given).
   reloadContent: (id = null)->
-    dialog = if id == null then $('div.niftyDialog:last') else $("div.niftyDialog#dialog-#{id}")
+    dialog = if id == null then $('div.niftyDialog:last') else $("div.niftyDialog#niftyDialog-#{id}")
     options = dialog.data('options')
     if options.url?
       $.ajax


Simplify mode.coffee

Simplify mode.coffee

--- content_scripts/mode.coffee
+++ content_scripts/mode.coffee
@@ -57,9 +57,13 @@
     if @options.suppressAllKeyboardEvents
       # TODO(philc): Make a let statement.
       downHanlder = @options["keydown"]
-      @options["keydown"] = (event) => @alwaysSuppressPropagation => if downHanlder then downHanlder(event)
+      @options["keydown"] = (event) => @alwaysSuppressPropagation =>
+        if downHanlder
+          downHanlder(event)
       pressHandler = @options["keypress"]
-      @options["keypress"] = (event) => @alwaysSuppressPropagation => if pressHandler then pressHandler(event)
+      @options["keypress"] = (event) => @alwaysSuppressPropagation =>
+        if pressHandler
+          pressHandler(event)

     @push
       keydown: @options.keydown || null
@@ -71,9 +75,13 @@
         # undefined, then the request propagates to the next mode.
         # The active indicator can also be changed with @setIndicator().
         if @options.indicator?
-          if @options.indicator then HUD.show @options.indicator else HUD.hide true, false
+          if @options.indicator
+            HUD.show @options.indicator
+          else
+            HUD.hide true, false
           @passEventToPage
-        else @continueBubbling
+        else
+          @continueBubbling

     # If @options.exitOnEscape is truthy, then the mode will exit when the escape key is pressed.
     if @options.exitOnEscape
@@ -82,7 +90,8 @@
       @push
         _name: "mode-#{@id}/exitOnEscape"
         "keydown": (event) =>
-          return @continueBubbling unless KeyboardUtils.isEscape event
+          unless KeyboardUtils.isEscape event
+            return @continueBubbling
           @exit event, event.target
           @suppressEvent

@@ -91,7 +100,9 @@
     if @options.exitOnBlur
       @push
         _name: "mode-#{@id}/exitOnBlur"
-        "blur": (event) => @alwaysContinueBubbling => @exit event if event.target == @options.exitOnBlur
+        "blur": (event) => @alwaysContinueBubbling =>
+          if event.target == @options.exitOnBlur
+            @exit event

     # If @options.exitOnClick is truthy, then the mode will exit on any click event.
     if @options.exitOnClick
@@ -143,6 +154,7 @@
     Mode.modes.push this
     @setIndicator()
     @logModes()
+    return
     # End of Mode constructor.

   setIndicator: (indicator = @options.indicator) ->
@@ -166,12 +178,15 @@
     return

   exit: (args...) ->
-    return if @modeIsExiting or not @modeIsActive
+    if @modeIsExiting or not @modeIsActive
+      return
     @log "deactivate:", @id
     @modeIsExiting = true

-    handler args... for handler in @exitHandlers
-    handlerStack.remove handlerId for handlerId in @handlers
+    for handler in @exitHandlers
+      handler args...
+    for handlerId in @handlers
+      handlerStack.remove handlerId
     Mode.modes = Mode.modes.filter (mode) => mode != this

     @modeIsActive = false
@@ -181,11 +196,13 @@
   logModes: ->
     if Mode.debug
       @log "active modes (top to bottom):"
-      @log " ", mode.id for mode in Mode.modes[..].reverse()
+      for mode in Mode.modes[..].reverse()
+        @log " ", mode.id
     return

   log: (args...) ->
-    console.log args... if Mode.debug
+    if Mode.debug
+      console.log args...
     return

   # For tests only.
@@ -194,7 +211,8 @@

   # For tests only.
   @reset: ->
-    mode.exit() for mode in @modes
+    for mode in @modes
+      mode.exit()
     @modes = []
     return


Only run spec on Mac and Linux

Only run spec on Mac and Linux

--- spec/pathwatcher-spec.coffee
+++ spec/pathwatcher-spec.coffee
@@ -115,18 +115,17 @@
       fs.writeFileSync(tempFile, 'changed')

   describe 'when watching a file that does not exist', ->
-    it 'throws an error with a code', ->
+    it 'throws an error with a code #darwin #linux', ->
       doesNotExist = path.join(tempDir, 'does-not-exist')
       watcher = null
       try
         watcher = pathWatcher.watch doesNotExist, -> null
-      catch e
-        expect(e.message).toBe 'Unable to watch path'
-        expect(e.errno).toBe require('constants').ENOENT
-        if process.version.match /^v0\.1[12]/
-          expect(e.code).toBe 'ENOENT'
+      catch error
+        expect(error.message).toBe 'Unable to watch path'
+        expect(error.errno).toBe require('constants').ENOENT
+        if error.code?
+          expect(error.code).toBe 'ENOENT'
       expect(watcher).toBe null  # ensure it threw
-

   describe 'when watching multiple files under the same directory', ->
     it 'fires the callbacks when both of the files are modifiled', ->


Upgrade vendored SpacePen to d9b6b4a2fe3fdeca07bb82e14e412718540f89f3

--- vendor/space-pen.coffee
+++ vendor/space-pen.coffee
@@ -1,4 +1,4 @@
-# Modified from f6ef4ba2ffdc27b6c9a057560f1f527b1e0f3e8b
+# Modified from d9b6b4a2fe3fdeca07bb82e14e412718540f89f3
 $ = jQuery = require('jquery')

 elements =
@@ -199,7 +199,7 @@
 jQuery.cleanData = (elements) ->
   for element in elements
     view = $(element).view()
-    view.afterRemove?() if view?[0] == element
+    view.afterRemove?() if view and view?[0] == element
   originalCleanData(elements)

 (exports ? this).View = View


trigger loader on lightbox view

trigger loader on lightbox view

--- components/lightbox/client/lightbox_view.coffee
+++ components/lightbox/client/lightbox_view.coffee
@@ -17,10 +17,14 @@
     $('body').addClass 'is-lightbox'
     @$el.addClass 'is-active'

+    mediator.trigger 'load:start'
+
     @model.on "sync", @render, @
     @model.fetch()

   render: ->
+    mediator.trigger 'load:stop'
+
     @$el.html lightboxTemplate(block: @model)

     feed = new Feed null,


:art: Improve test descriptions and fix a typo

--- spec/text-editor-spec.coffee
+++ spec/text-editor-spec.coffee
@@ -1976,7 +1976,7 @@

               expect(editor.lineTextForBufferRow(7)).toBe "    var pivot = items.shift(), current, left = [], right = [];"

-        describe "when the slecetion spans multiple lines", ->
+        describe "when the selection spans multiple lines", ->
           it "moves the lines spanned by the selection to the following row", ->
             expect(editor.lineTextForBufferRow(2)).toBe "    if (items.length <= 1) return items;"
             expect(editor.lineTextForBufferRow(3)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
@@ -2044,8 +2044,8 @@
               expect(editor.lineTextForBufferRow(5)).toBe "      current < pivot ? left.push(current) : right.push(current);"
               expect(editor.lineTextForBufferRow(6)).toBe "      current = items.shift();"

-          describe "when there is a fold", ->
-            it "moves all lines that spanned by a selection to preceding row, preserving all folds", ->
+          describe "when there is a fold below one of the selected row", ->
+            it "moves all lines spanned by a selection to the following row, preserving the fold", ->
               editor.createFold(4, 7)

               expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2067,28 +2067,28 @@
               expect(editor.lineTextForBufferRow(7)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
               expect(editor.lineTextForBufferRow(9)).toBe "    return sort(left).concat(pivot).concat(sort(right));"

-            describe "and the multiple selections spans the two line before it", ->
-              it "moves all the lines, preserving the fold", ->
-                editor.createFold(4, 7)
-
-                expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
-
-                editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]])
-                editor.moveLineDown()
-
-                expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [7, 4]], [[6, 2], [6, 6]]]
-                expect(editor.lineTextForBufferRow(2)).toBe "    while(items.length > 0) {"
-                expect(editor.isFoldedAtBufferRow(2)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
-                expect(editor.isFoldedAtBufferRow(6)).toBeFalsy()
-                expect(editor.lineTextForBufferRow(6)).toBe "    if (items.length <= 1) return items;"
-                expect(editor.lineTextForBufferRow(7)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+          describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", ->
+            it "moves all the lines below the fold, preserving the fold", ->
+              editor.createFold(4, 7)
+
+              expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(7)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(8)).toBeFalsy()
+
+              editor.setSelectedBufferRanges([[[2, 2], [2, 6]], [[3, 0], [3, 4]]])
+              editor.moveLineDown()
+
+              expect(editor.getSelectedBufferRanges()).toEqual [[[7, 0], [7, 4]], [[6, 2], [6, 6]]]
+              expect(editor.lineTextForBufferRow(2)).toBe "    while(items.length > 0) {"
+              expect(editor.isFoldedAtBufferRow(2)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(3)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
+              expect(editor.isFoldedAtBufferRow(6)).toBeFalsy()
+              expect(editor.lineTextForBufferRow(6)).toBe "    if (items.length <= 1) return items;"
+              expect(editor.lineTextForBufferRow(7)).toBe "    var pivot = items.shift(), current, left = [], right = [];"

         describe "when some of the selections span the same lines", ->
           it "moves lines that contain multiple selections correctly", ->


Moved to lower key for os.

Moved to lower key for os.

--- app/assets/javascripts/app/lib/app_post/browser.coffee
+++ app/assets/javascripts/app/lib/app_post/browser.coffee
@@ -3,7 +3,7 @@
     data =
       browser: @searchString(@dataBrowser) or "An unknown browser"
       version: @searchVersion(navigator.userAgent) or @searchVersion(navigator.appVersion) or "an unknown version"
-      OS:      @searchString(@dataOS) or "an unknown OS"
+      os:      @searchString(@dataOS) or "an unknown os"

   @check: ->
     data = @detection()


Factor out infowindow logic into strategy object

--- app/assets/javascripts/google_map/marker_builder.js.coffee
+++ app/assets/javascripts/google_map/marker_builder.js.coffee
@@ -1,6 +1,31 @@
+class LocalSupport.LargeOrgInfoWindowBuilder
+  infobox: (boxText)->
+    content: boxText
+    pixelOffset: new google.maps.Size(-151,-102)
+  klass: ->
+    "arrow_box"
+
+class LocalSupport.SmallOrgInfoWindowBuilder
+  infobox: (boxText)->
+    content: boxText
+    pixelOffset: new google.maps.Size(-151,-84)
+  klass: ->
+    "arrow_box"
+
+class LocalSupport.VolOpInfoWindowBuilder
+  infobox: (boxText)->
+    content: boxText
+    pixelOffset: new google.maps.Size(-151,10)
+  klass: ->
+    "arrow_box_vol_op"
+
+
 class LocalSupport.MarkerBuilder extends Gmaps.Google.Builders.Marker
   create_marker: ->
-    @type = this.args.type
+    @infoWindowBuilder = switch this.args.type
+      when 'vol_op' then new LocalSupport.VolOpInfoWindowBuilder
+      when 'small_org' then new LocalSupport.SmallOrgInfoWindowBuilder
+      when 'large_org' then new LocalSupport.LargeOrgInfoWindowBuilder
     options = _.extend @marker_options(), @rich_marker_options()
     @serviceObject = new RichMarker options

@@ -18,8 +43,7 @@
     return null unless _.isString @args.custom_infowindow

     boxText = document.createElement("div")
-    klass = if (@type == 'vol_op') then 'arrow_box_vol_op' else 'arrow_box'
-    boxText.setAttribute("class", klass)
+    boxText.setAttribute("class", @infoWindowBuilder.klass())
     boxText.innerHTML = @args.custom_infowindow
     @infowindow = new InfoBox(@infobox(boxText))
     @addCloseHandler(@infowindow)
@@ -32,9 +56,4 @@
     infowindow

   infobox: (boxText)->
-    offset = switch @type
-      when 'large_org' then new google.maps.Size(-151, -102)
-      when 'small_org' then new google.maps.Size(-151, -84)
-      when 'vol_op' then new google.maps.Size(-151,10)
-    content: boxText
-    pixelOffset: offset
+    @infoWindowBuilder.infobox boxText


Replaced searches with calls to findSvc angularjs service.

It's better if the searching code is centralized in findSvc, for easier maintenance.

--- app/assets/javascripts/ng-services/ng-CleanupSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-CleanupSvc.js.coffee
@@ -57,23 +57,19 @@
           return false

         # Feeds without unread entries with a refresh feed alert in the start page are not removed
-        refresh_job_states = $filter('filter') $rootScope.refresh_feed_job_states, (job_state)->
-          # Find refresh_feed_job_states for this feed
-          return job_state.feed_id == feed.id
-        if refresh_job_states? && refresh_job_states?.length > 0
-          return false
+        refresh_job_states = findSvc.find_feed_refresh_jobs feed.id
+        return false if refresh_job_states? && refresh_job_states?.length > 0

         # Feeds without unread entries with a subscribe alert in the start page are not removed
-        subscribe_job_states = $filter('filter') $rootScope.subscribe_job_states, (job_state)->
-          # Find subscribe_job_states for this feed
-          return job_state.feed_id == feed.id
-        if subscribe_job_states? && subscribe_job_states?.length > 0
-          return false
+        subscribe_job_states = findSvc.find_feed_subscribe_jobs feed.id
+        return false if subscribe_job_states? && subscribe_job_states?.length > 0

         # The rest of feeds are removed
         return true
+
       if read_feeds? && read_feeds?.length > 0
         for feed in read_feeds
+          # The current feed, or feeds in the current folder, are not removed
           if $rootScope.current_feed?.id != feed.id && $rootScope.current_folder?.id != feed.folder_id
             # Delete feed from the scope
             index = $rootScope.feeds.indexOf feed


do not allow to start several NProgress, set document title with current error if any, remove protocol from pjax url, submit basics are now there for simple forms (still need modals)

--- app/assets/javascripts/flammarion_rails/actions.coffee
+++ app/assets/javascripts/flammarion_rails/actions.coffee
@@ -4,20 +4,27 @@

 ws.send_before_actions.push (data) ->
   if NProgress?
-    window.progress_bar_timeout = setTimeout(->
-      NProgress.start()
-    , 500)
+    unless window.progress_bar_timeout?
+      window.progress_bar_timeout = setTimeout(->
+        NProgress.start()
+      , 500)

 ws.onmessage_before_actions.push (event) ->
   if NProgress?
     NProgress.done()
     clearTimeout(window.progress_bar_timeout)
+    window.progress_bar_timeout = null
+
+# Error
+
+ws.onmessage_actions.error = (event) ->
+  document.title = ws_data.title

 # Pjax

 $(document).on 'pjax:beforeSend', (event, xhr, settings) ->
   event.preventDefault()
-  ws.send(action: 'pjax', url: settings.url)
+  ws.send(action: 'pjax', url: settings.url.replace(/^.*:\/{2}/i, ''))

 ws.onmessage_actions.pjax = (event) ->
   new_page = $("<div>")
@@ -31,4 +38,8 @@
 $(document).on 'submit', (event) ->
   event.preventDefault()
   form = $(event.target)
-  #TODO ws.send(action: 'submit', form: form.serialize())
+  return if form.hasClass 'pjax-form'
+  button_name = document.activeElement.name
+  ws.send(action: 'submit', url: form.attr('action'), method: 'post', form: form.serialize(), "#{button_name}": '')
+
+ws.onmessage_actions.submit = ws.onmessage_actions.pjax


Atom config

Atom config

--- atom/config.cson
+++ atom/config.cson
@@ -19,4 +19,3 @@
     'fontFamily': 'Menlo'
     'fontSize': 14
     'invisibles': {}
-  'linter': {}


Update config.cson

Update config.cson

--- atom/config.cson
+++ atom/config.cson
@@ -39,14 +39,18 @@
     tabType: "soft"
   emmet: {}
   "exception-reporting":
-    userId: "redact"
+    userId: ""
   linter:
     lintDebug: true
     showInfoMessages: true
     statusBar: "Show all errors"
   "linter-eslint":
-    globalNodePath: "/usr/local"
-    useGlobalEslint: true
+    lintHtmlFiles: true
+    rulesToSilenceWhileTyping: [
+      "no-trailing-spaces"
+      "eol-last"
+      "semi"
+    ]
   "linter-flake8":
     executablePath: "/usr/local/bin/flake8"
     flakeErrors: true


Update atom

Update atom

--- atom/config.cson
+++ atom/config.cson
@@ -339,9 +339,8 @@
           fb_buck_format:
             enabled: true
           fb_dataswarm_format:
-            enabled: true
-          fb_dataswarm_linter:
-            enabled: true
+            enabled: false
+          fb_dataswarm_linter: {}
           fb_dataswarm_macros_definition:
             enabled: true
           fb_dataswarm_macros_hover:


Add custom command for inserting a date stamp

--- atom/init.coffee
+++ atom/init.coffee
@@ -5,6 +5,10 @@
 oldWindowDimensions = {}

 atom.commands.add 'atom-workspace',
+  'custom:insert-timestamp': ->
+    now = new Date()
+    atom.workspace.getActiveTextEditor().insertText(now.toISOString())
+
   'custom:open-todo-list': ->
     todoList = path.join(process.env.HOME, 'Dropbox/todo/todo.txt')
     atom.workspace.open(todoList)


Add better key map for Quippet

Add better key map for Quippet

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -21,3 +21,5 @@
 '.platform-darwin':
   'ctrl-cmd-r': 'tree-view:reveal-active-file'
   'ctrl-cmd-t': 'window:run-package-specs'
+'.workspace':
+  'ctrl-shift-q': 'quippet:toggle'


Map command-delete to tree-view:remove

Map command-delete to tree-view:remove

Technically command-backspace, but whatever.

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -127,3 +127,6 @@

   # Rename the selected file or directory
   'cmd-e': 'tree-view:rename'
+
+  # Remove the selected file or directory
+  'cmd-backspace': 'tree-view:remove'


Clear mini editor base select list cancelled()

Clearing the mini editor when closing is something
all sub-classes were already doing so it makes sense
to pull it up to the base class as the default
cancelled() implementation that can still be overridden
if needed.

--- src/autocomplete-view.coffee
+++ src/autocomplete-view.coffee
@@ -74,10 +74,10 @@
     @editor.setCursorBufferPosition([position.row, position.column + match.suffix.length])

   cancelled: ->
+    super
+
     @editor.abort()
     @editor.setSelectedBufferRange(@originalSelectionBufferRange)
-
-    @miniEditor.setText('')
     @editor.rootView()?.focus() if @miniEditor.isFocused

   attach: ->


Replace  with

Replace  with

--- app/assets/javascripts/components/users/base.js.coffee
+++ app/assets/javascripts/components/users/base.js.coffee
@@ -9,14 +9,15 @@
     tooltipClass = '.information-tooltip'
     $(document).on('click', "#{tooltipClass}-trigger", (ev) ->
       ev.preventDefault()
-      if ($(@).find('.fa-align-left').length)
+      $this = $(@)
+      if ($this.find('.fa-align-left').length)
         $("#{tooltipClass}-trigger").not(@).siblings(tooltipClass).hide()
         $("#{tooltipClass}-trigger").not(@).html(' details')
-        $(@).html(' close')
-        $(@).siblings(tooltipClass).show()
+        $this.html(' close')
+        $this.siblings(tooltipClass).show()
       else
-        $(@).html(' details')
-        $(@).siblings(tooltipClass).hide()
+        $this.html(' details')
+        $this.siblings(tooltipClass).hide()
     )

 $(document).ready -> UsersComponent.initialize()


Changed colour for Fix and Include tasks in task distribution chart

--- app/assets/javascripts/task_status_breakdown.js.coffee
+++ app/assets/javascripts/task_status_breakdown.js.coffee
@@ -38,15 +38,15 @@

 taskDistributionData = (tasks) ->
   distributionData = [
-    { key: "Not Submitted",     values: [], color: "#999999"}
-    { key: "Need Help",         values: [], color: "#F6A895"}
-    { key: "Working On It",     values: [], color: "#FCEC21"}
-    { key: "Fix and Resubmit",  values: [], color: "#FBB450"}
-    { key: "Fix and Include",  values: [], color: "#FBB450"}
-    { key: "Redo",     values: [], color: "#B06C4E"}
-    { key: "Awaiting Signoff",  values: [], color: "#0074CC"}
-    { key: "Complete",          values: [], color: "#62C462"}
-  ]
+    { key: "Not Submitted",     values: [], color: "#999999" }
+    { key: "Need Help",         values: [], color: "#F6A895" }
+    { key: "Working On It",     values: [], color: "#FCEC21" }
+    { key: "Fix and Resubmit",  values: [], color: "#FBB450" }
+    { key: "Fix and Include",   values: [], color: "#FFD119" }
+    { key: "Redo",              values: [], color: "#B06C4E" }
+    { key: "Awaiting Signoff",  values: [], color: "#0074CC" }
+    { key: "Complete",          values: [], color: "#62C462" }
+  ]

   taskStatuses = [
     "not_submitted"


Remove settings paramètres test.

Remove settings paramètres test.

--- js/flight.coffee
+++ js/flight.coffee
@@ -173,7 +173,7 @@

   $ ->

-    window.flight = new Flight({speed:"0.4s"});
+    window.flight = new Flight();

     $('.back').on 'click', =>
         flight.goToPage('#panel-1')


change api call for fetching schema's cases

--- src/app/cases/cases.js.coffee
+++ src/app/cases/cases.js.coffee
@@ -113,8 +113,9 @@
         $scope.cases = resp
     else if $stateParams.case_filter == 'schema'
       if schema
-        schema.all('cases').getList().then (resp)->
-          $scope.cases = resp
+        Restangular.all('cases').getList({schema_id: schema.id})
+          .then (resp)->
+            $scope.cases = resp

     $scope.go = (id)->
       $state.go('edit_case',{case_id: id})


add site-id class to selection-view

add site-id class to selection-view

--- src/app/selection-view.coffee
+++ src/app/selection-view.coffee
@@ -17,6 +17,9 @@
     @selection.on 'destroyed', =>
       @needsRemoval = true
       @editor.requestDisplayUpdate()
+
+    if @selection.marker.isRemote()
+      @addClass("site-#{@selection.marker.getOriginSiteId()}")

   updateDisplay: ->
     @clearRegions()


breakcost fix

breakcost fix

--- js/render.coffee
+++ js/render.coffee
@@ -3,6 +3,7 @@
  (Base, TYPES) ->

    PREFIX = 'wallapatta_'
+   BLOCK_LEVEL = 10

    INF = 1e10

@@ -26,6 +27,8 @@
     4: 75
     5: 108
     6: 155
+    "#{BLOCK_LEVEL}": 1000
+

    PARENT_POSITION_COST = 500
@@ -68,16 +71,12 @@
      parent = node.parent()
      cost = 0

-     if parent?
+     while parent?
       cost += @getNodeBreakCost parent
-      while parent?
-       pos = (@getOffsetTop node.elem, @elems.main) -
-             (@getOffsetTop parent.elem, @elems.main)
-       cost += @_parentPositionCost pos
-       parent = parent.parent()
-     else
-      if node.type isnt 'article'
-       throw new Error 'Node without a parent'
+      pos = (@getOffsetTop node.elem, @elems.main) -
+            (@getOffsetTop parent.elem, @elems.main)
+      cost += @_parentPositionCost pos
+      parent = parent.parent()

      return cost


add indexes to db

add indexes to db

--- services/clsi/app/coffee/db.coffee
+++ services/clsi/app/coffee/db.coffee
@@ -16,11 +16,20 @@
 		url: Sequelize.STRING
 		project_id: Sequelize.STRING
 		lastModified: Sequelize.DATE
+	}, {
+		indexes: [
+			{fields: ['url', 'project_id']},
+			{fields: ['project_id']}
+		]
 	})

 	Project: sequelize.define("Project", {
-		project_id: Sequelize.STRING
+		project_id: {type: Sequelize.STRING, primaryKey: true}
 		lastAccessed: Sequelize.DATE
+	}, {
+		indexes: [
+			{fields: ['lastAccessed']}
+		]
 	})

 	sync: () -> sequelize.sync()


Else.

Else.

--- js2coffee.coffee
+++ js2coffee.coffee
@@ -134,8 +134,11 @@
     @indent (i) =>
       test = @walk(node.test)
       consequent = @walk(node.consequent)
+      alt = []
+      if node.alternate
+        alt = [ 'else', "\n", @walk(node.alternate) ]

-      [ i, 'if ', test, "\n", consequent ]
+      [ i, 'if ', test, "\n", consequent, alt ]

   BlockStatement: (node) ->
     node.body.map(@walk)


Use markers instead of anchors to track which close-brackets to skip

--- src/app/language-mode.coffee
+++ src/app/language-mode.coffee
@@ -18,7 +18,7 @@
   constructor: (@editSession) ->
     @buffer = @editSession.buffer
     @reloadGrammar()
-    @bracketAnchorRanges = []
+    @bracketMarkers = []

     _.adviseBefore @editSession, 'insertText', (text) =>
       return true if @editSession.hasMultipleCursors()
@@ -37,19 +37,19 @@
       autoCompleteOpeningBracket = @isOpeningBracket(text) and not hasWordAfterCursor and not (@isQuote(text) and hasWordBeforeCursor)
       skipOverExistingClosingBracket = false
       if @isClosingBracket(text) and nextCharacter == text
-        if bracketAnchorRange = @bracketAnchorRanges.filter((anchorRange) -> anchorRange.getBufferRange().end.isEqual(cursorBufferPosition))[0]
+        if bracketMarker = _.find(@bracketMarkers, (marker) => @editSession.getMarkerBufferRange(marker).end.isEqual(cursorBufferPosition))
           skipOverExistingClosingBracket = true

       if skipOverExistingClosingBracket
-        bracketAnchorRange.destroy()
-        _.remove(@bracketAnchorRanges, bracketAnchorRange)
+        @editSession.destroyMarker(bracketMarker)
+        _.remove(@bracketMarkers, bracketMarker)
         @editSession.moveCursorRight()
         false
       else if autoCompleteOpeningBracket
         @editSession.insertText(text + @pairedCharacters[text])
         @editSession.moveCursorLeft()
         range = [cursorBufferPosition, cursorBufferPosition.add([0, text.length])]
-        @bracketAnchorRanges.push @editSession.addAnchorRange(range)
+        @bracketMarkers.push @editSession.markBufferRange(range)
         false

     _.adviseBefore @editSession, 'backspace', =>


[browser] AppWindow now delegate BrowserWindow's events

--- src/browser/AppWindow.coffee
+++ src/browser/AppWindow.coffee
@@ -24,8 +24,35 @@
         return

     handleEvents    : ->
-        @browserWindow.on "closed", @dispose.bind(@)
-        @browserWindow.on "focus", =>
+        # delegate browserWindow events
+        # https://github.com/atom/electron/blob/02bdace366f38271b5c186412f42810ecb06e99e/docs/api/browser-window.md
+        [
+            "page-title-updated"
+            "close"
+            "closed"
+            "unresponsive"
+            "responsive"
+            "blur"
+            "focus"
+            "maximize"
+            "unmaximize"
+            "minimize"
+            "restore"
+            "resize"
+            "move"
+            "moved"
+            "enter-full-screen"
+            "leave-full-screen"
+            "enter-html-full-screen"
+            "leave-html-full-screen"
+            "devtools-opened"
+            "devtools-closed"
+            "devtools-focused"
+        ].forEach (name) =>
+            @browserWindow.on name, => @emit name, arguments...
+
+        @on "closed", @dispose.bind(@)
+        @on "focus", =>
             global.app.setLastFocusedWindow @

         if @options.devMode


Forgot to rename some templates. Good now.

--- client/client.coffee
+++ client/client.coffee
@@ -24,7 +24,7 @@

 # The Create Template
-Template.create.events
+Template.volunteersCreate.events
   'click input[type=button]': (event, template) ->
     fileinput = template.find('[type=file]')
     options =
@@ -51,5 +51,5 @@

 # The List Template
-Template.list.volunteers = ->
+Template.volunteersList.volunteers = ->
   return Volunteers.find()


remove Location require to prevent circular import

--- src/components/exercise.cjsx
+++ src/components/exercise.cjsx
@@ -11,12 +11,10 @@
 ExerciseTags = require 'components/exercise/tags'
 Question = require 'components/exercise/question'
 Attachments = require 'components/exercise/attachments'
-Location = require 'stores/location'

 Exercise = React.createClass
   propTypes:
     id:   React.PropTypes.string.isRequired
-    location: React.PropTypes.instanceOf(Location)

   getInitialState: -> {}
   update: -> @forceUpdate()


use source-map for production, override development's built-in ones

otherwise build is 13mb :(

--- configs/webpack/base.coffee
+++ configs/webpack/base.coffee
@@ -140,6 +140,7 @@

   productionBase =
     output: output
+    devtool: 'source-map'
     module:
       rules: BASE_BUILD_LOADERS
     plugins: makeBuildPlugins({styleFilename}).concat([


register for disable, not enable. come on Emiliano.

--- content/BetterBibTeX.coffee
+++ content/BetterBibTeX.coffee
@@ -59,7 +59,7 @@

       return
   }
-  uninstaller.onEnabling = uninstaller.onUninstalling
+  uninstaller.onDisabling = uninstaller.onUninstalling
   AddonManager.addAddonListener(uninstaller)

   ###


set lastLogin on login

set lastLogin on login

--- src/controllers/users.coffee
+++ src/controllers/users.coffee
@@ -13,6 +13,8 @@
       req.flash 'error', 'Invalid username or password.'
       res.redirect '/login'
     else
+      user.lastLogin = new Date
+      user.save()
       req.session.user = user
       res.redirect '/'


Support LEN and LENB

Support LEN and LENB

--- src/formula_evaluator.coffee
+++ src/formula_evaluator.coffee
@@ -509,6 +509,16 @@
       len = @expectInteger(args[1])

     multibyteSubstr(str, 0, len)
+
+  LEN: (args)->
+    @checkArgumentSize(args, 1)
+    str = @expectString(args[0])
+    str.length
+
+  LENB: (args)->
+    @checkArgumentSize(args, 1)
+    str = @expectString(args[0])
+    multibyteLength(str)

   LN: (args)->
     @checkArgumentSize(args, 1)


Bumping version to 1.1.4-pre

Bumping version to 1.1.4-pre

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -21,7 +21,7 @@
   require.registerExtension '.coffee', (content) -> compile content

 # The current CoffeeScript version number.
-exports.VERSION = '1.1.3'
+exports.VERSION = '1.1.4-pre'

 # Words that cannot be used as identifiers in CoffeeScript code
 exports.RESERVED = RESERVED


Eliminate "watchlist" word (addresses http://us.chlk.it/watchlist?results=true)

Former-commit-id: c37b3ab50bedc2a65854761443d44d293380cebd [formerly 31422fd51e42f60d3da955d2f5272d1a983b6fc2] [formerly 5d3f8c6addb417857c9c5153b53aab46778525f5 [formerly 5ea71af52105a81dc615c6992ceb7296bd886c12]] [formerly f0fdd2ad8b2c3af43554e7c8815382f2d26fceb6 [formerly 0325231bdbf5316ef05c4e1f1ddd7d93864b09fe] [formerly 212b5c5469c48a2eaa8d4c22b232bebbd3f6581d [formerly 212b5c5469c48a2eaa8d4c22b232bebbd3f6581d [formerly 7d86aecbd4c299ffcb096925be33bb489ea99679]]]]
Former-commit-id: 4608b4334a22147e04a125d6dbe68b10651556f6 [formerly 95c67167cda1b30116ee53573e924ddd2004e84b] [formerly f0566bac9da290d56df1c0d8b469782280e3c484 [formerly 0d6db93af32513b2e94467fd978f008d79aa9c9e]]
Former-commit-id: c78f3f3206b5fa3e781aa2fac3f5cd863d7735e0 [formerly c2ca964e0ef438ae75e25f06262b8431ea357ae6]
Former-commit-id: 2c874afd618d4394cded0d03367eb0bfde0b937b
Former-commit-id: eb5afb317116898dbf45b9b8dfaf60597772e917

--- @client/swapables.coffee
+++ @client/swapables.coffee
@@ -190,18 +190,18 @@

                   DIV style: first_column,
                     if current_user
-                      # ability to add to watchlist
-
-                      in_watchlist = current_user.subscriptions[proposal.key] == 'watched'
+                      # ability to watch proposal
+
+                      watching = current_user.subscriptions[proposal.key] == 'watched'
                       I
-                        className: "fa #{if in_watchlist then 'fa-star' else 'fa-star-o'}"
-                        title:  if in_watchlist
-                                  "Remove from your watchlist"
+                        className: "fa #{if watching then 'fa-star' else 'fa-star-o'}"
+                        title:  if watching
+                                  "You are watching this proposal"
                                 else
-                                  "Add this proposal to your watchlist"
+                                  "Watch this proposal"
                         style:
-                          opacity: if @local.hover_watch != proposal.key && !in_watchlist then .35
-                          color: if in_watchlist then logo_red else "#888"
+                          opacity: if @local.hover_watch != proposal.key && !watching then .35
+                          color: if watching then logo_red else "#888"
                           width: 30
                           height: 30
                           position: 'absolute'


Fix checkEmail to prevent duplicate checks

--- app/views/core/CreateAccountModal/BasicInfoView.coffee
+++ app/views/core/CreateAccountModal/BasicInfoView.coffee
@@ -56,7 +56,7 @@

   checkEmail: ->
     email = @$('[name="email"]').val()
-    if email is @state.get('lastEmailValue')
+    if email is @state.get('checkEmailValue')
       return @state.get('checkEmailPromise')

     if not (email and forms.validateEmail(email))
@@ -65,7 +65,7 @@
         checkEmailValue: email
         checkEmailPromise: null
       })
-      return
+      return Promise.resolve()

     @state.set({
       checkEmailState: 'checking'


Update the Safari user agent string to 5.1.

--- application/chrome/content/wesabe/xul/UserAgent.coffee
+++ application/chrome/content/wesabe/xul/UserAgent.coffee
@@ -22,7 +22,7 @@
     wesabe.util.prefs.set("general.useragent.vendor", ua.vendor) if ua.vendor
     wesabe.util.prefs.set("general.useragent.vendorSub", ua.vendorSub) if ua.vendorSub

-    wesabe.info("User Agent changed to ", @toString)
+    wesabe.info "User Agent changed to ", @toString()

   #
   # Set the User Agent string by an alias.
@@ -59,11 +59,11 @@
       useragent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.4) Gecko/2008102920 Firefox/3.0.4",
       vendor: "",
       vendorSub: "",
-    else if /^(safari)\s*(3[\.\d]*)?$/i.test(alias)
+    else if /^(safari)\s*(5[\.\d]*)?$/i.test(alias)
       appname: "Netscape",
-      appversion: "5.0 (Macintosh; U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.2 Safari/525.26.12",
+      appversion: "5.0 (Macintosh; Intel Mac OS X 10_7) AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 Safari/534.48.3",
       platform: "MacIntel",
-      useragent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.2 Safari/525.26.12",
+      useragent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7) AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 Safari/534.48.3",
       vendor: "Apple Computer, Inc.",
       vendorSub: "",


moved onRender and onConnect params to main sharejs class

--- sharejs-base/sharejs-client.coffee
+++ sharejs-base/sharejs-client.coffee
@@ -18,6 +18,10 @@

     @isCreated = false
     @docIdVar = docIdVar
+
+    params = Blaze.getData(parentView)
+    @configCallback = params.onRender
+    @connectCallback = params.onConnect

   create: ->
     throw new Error("Already created") if @isCreated
@@ -79,12 +83,14 @@
     @isDestroyed = true

 class ShareJSTextConnector extends ShareJSConnector
+
   createView: ->
     return Blaze.With(Blaze.getData, -> Template._sharejsText)

   rendered: (element) ->
     super
     @textarea = element
+    @configCallback?(@textarea)

   connect: ->
     @textarea.disabled = true
@@ -94,6 +100,7 @@
     super
     doc.attach_textarea(@textarea)
     @textarea.disabled = false
+    @connectCallback?(@textarea)

   disconnect: ->
     @textarea?.detach_share?()


准备好上传表单

准备好上传表单

--- client/components/uploadForm/uploadForm.controller.coffee
+++ client/components/uploadForm/uploadForm.controller.coffee
@@ -18,7 +18,7 @@

   # form submit
   $scope.submit = ->
-    if form.file.$valid && $scope.file && !$scope.file.$error
+    if $scope.form.file.$valid && $scope.form.file
       $scope.upload($scope.file)

   $scope.upload = (file) ->


Need process node.

Need process node.

--- jquery.colorfy.coffee
+++ jquery.colorfy.coffee
@@ -1,49 +1,88 @@
 # simple markdown parser rules
-# /^\s*#.*$/g -> title
-# /^\s*[=-]+\s*$/g -> title separator
-# /^\s*>.*$/g -> block
-# /([\*_]).+?\1/gm -> italic
-# /[\*_]{2}.+?\1/gm -> bold
-# /~~.+?~~/gm -> strikethrough
-# NONE -> underscore
-# /^\s[1-9]+\. .+$/g -> ordered list
-# /^\s*[*+-] .+$/g -> unordered list
 # /[.+](.+)/g -> inline link
 # /[.+][.+]/g -> reference link
 # /[.+]/g -> abbr link !BUG
 # /[.+]: .+/g -> link or image reference
 # //g -> inline image
 # //g -> reference image
-# /`.+?`/gm -> inline code
-# /```.+```/gm -> code block
-# table border
-# horizontal rule
-# inline html
+markdownSyntaxDescriptor =
+  "title":         [/^\s{0,3}\#{1,6}.*$/m, /^.+?\n[=-]{2,}\s*$/m]
+  "block":         /^\s{0,3}>\s+.*$/m
+  "emphasis":      /([\*_]).+?\1/m
+  "strong":        /([\*_]{2}).+?\1/m
+  "strikethrough": /~~.+?~~/m
+  "orderedlist":   /^\s*[0-9]+\. .+$/m
+  "unorderedlist": /^\s*[*+-] .+$/m
+  "inlinecode":    /`.+?`/m
+  "codeblock":     [/```.+?```/m, /^(?: {4}|\t).+$/m]
+  "rule":          /^[-\*]{3,}/m
+  # "table":
+  # "inlinehtml"
+  # inline link
+  # reference link
+  # abbr link
+  # link or image reference
+  # inline image
+  # reference image

-dataTextToFormattedText = (dataText) ->
-  dataText = dataText.replace(/&/g, '&')   # & -> &
-  dataText = dataText.replace(/</g, '<')   # < -> <
-  dataText = dataText.replace(/>/g, '>')   # > -> >
+createNode = (content, htmlfier, descriptor, klass) ->
+  node = {}
+  node.content = content
+  node.htmlfier = htmlfier
+  node.descriptor = descriptor
+  node.klass = klass
+  node.subnodes = []
+  node.supernode = null # Unused
+  node.processed = false
+  node.terminate = false
+  node.toHTML = ->
+    node.process() unless node.processed
+    openSpan = if @klass then "" else ''
+    content = ''
+    if @terminate
+      content += @htmlfier(@content)
+    else
+      for node in @subnodes
+        content += node.toHTML()
+    closeSpan = if @klass then "" else ''
+    return openSpan + content + closeSpan
+
+  node.process = ->
+
+    node.processed = true
+
+  return node
+
+htmlfy = (dataText) ->
+  dataText = dataText.replace(/&/g, '&')    # & -> &
+  dataText = dataText.replace(/</g, '<')     # < -> <
+  dataText = dataText.replace(/>/g, '>')     # > -> >
   dataText = dataText.replace(/"/g, '"')   # " -> "
   dataText = dataText.replace(/'/g, ''')   # ' -> '
-  dataText = dataText.replace(/\//g, '/')   # / -> /
-  dataText = dataText.replace(/\n/g, '
')   # \n ->

-  dataText = dataText.replace(/ /g, ' ') # ' ' ->
-  # add span tag
+  dataText = dataText.replace(/\//g, '/')  # / -> /
+  dataText = dataText.replace(/\n/g, '
')    # \n ->

+  dataText = dataText.replace(/ /g, ' ')   # ' ' ->
   return dataText
+
+colorfy = (dataText, descriptor, htmlfier) ->
+  htmlfier ||= htmlfy
+  return htmlfier(dataText)
+
+dataTextToFormattedText = (dataText) ->
+  colorfy(dataText, markdownSyntaxDescriptor, htmlfy)

 formattedTextToDataText = (formattedText) ->
   formattedText = formattedText.replace(/<(?!br|\/br).+?>/gm, '') # strip tags
-  formattedText = formattedText.replace(/
/g, '\n')   #
 -> \n
-  formattedText = formattedText.replace(/</g, '<')   # < -> <
-  formattedText = formattedText.replace(/>/g, '>')   # > -> >
-  formattedText = formattedText.replace(/&/g, '&')   # & -> &
-  formattedText = formattedText.replace(/"/g, '"')   # &quot -> "
-  formattedText = formattedText.replace(/'/g, "'")   # &apos -> '
-  formattedText = formattedText.replace(/&#x2F/g, "/")   # &#x2F -> /
-  formattedText = formattedText.replace(/ /g, ' ') #   -> ' '
+  formattedText = formattedText.replace(/
/g, '\n')            #
 -> \n
+  formattedText = formattedText.replace(/</g, '<')             # < -> <
+  formattedText = formattedText.replace(/>/g, '>')             # > -> >
+  formattedText = formattedText.replace(/&/g, '&')            # & -> &
+  formattedText = formattedText.replace(/"/g, '"')           # &quot -> "
+  formattedText = formattedText.replace(/'/g, "'")           # &apos -> '
+  formattedText = formattedText.replace(/&#x2F/g, "/")            # &#x2F -> /
+  formattedText = formattedText.replace(/ /g, ' ')           #   -> ' '
   return formattedText

 $.fn.colorfy = (plainTextProcessor) ->
@@ -60,7 +99,7 @@
   div.css("overflow", "scroll")
   # Prevent enter to insert

   # See http://stackoverflow.com/questions/18552336/prevent-contenteditable-adding-div-on-enter-chrome
-  div.css("display", 'inline-block')
+  # div.css("display", 'inline-block')
   # Insert the fake text area
   this.after(div)

@@ -71,8 +110,16 @@
     div.data("content", area.val()).trigger("receive-content")

   div.on "receive-content", ->
+    if div.text().length == 0
+      div.css("display", "block")
+    else
+      div.css("display", "inline-block")
     div.html(dataTextToFormattedText(div.data("content")))
   div.on "send-content", ->
+    if div.text().length == 0
+      div.css("display", "block")
+    else
+      div.css("display", "inline-block")
     area.val(div.data("content"))

   div.on "keyup paste", ->


'return' bug and '*text* will jump' bug.

--- jquery.colorfy.coffee
+++ jquery.colorfy.coffee
@@ -1,3 +1,11 @@
+# Documentation
+#
+# For the Japanese / Chinese input method issue
+# http://stackoverflow.com/questions/1391278/contenteditable-change-events
+#
+# For the cursor issue
+# ;(
+
 # simple markdown parser rules
 # /[.+](.+)/g -> inline link
 # /[.+][.+]/g -> reference link
@@ -139,27 +147,29 @@
           location += cursorLocationFromNodeAndOffset(childNode, anchorNode, anchorOffset)
   return location

-nodeAndOffsetFromCursorLocation = (cursorLocation, rootNode) ->
-  if rootNode.nodeType == Node.TEXT_NODE
-    if rootNode.nodeValue.length >= cursorLocation
-      return [rootNode, cursorLocation]
-    else
-      return [null, rootNode.nodeValue.length]
-  else if rootNode.tagName == "BR"
-    if cursorLocation == 1
-      return [rootNode, 0]
-    return [null, 1]
-#  else if rootNode.tagName == "SPAN"
+lengthOfNode = (node) ->
+  if node.nodeType == Node.TEXT_NODE
+    return node.nodeValue.length
+  else if node.tagName == "BR"
+    return 1
+  else if (node.tagName == "SPAN") || (node.tagName == "DIV")
+    length = 0
+    for childNode in node.childNodes
+      length += lengthOfNode(childNode)
+    return length
+
+nodeAndOffsetFromCursorLocation = (location, node) ->
+  return [] if lengthOfNode(node) < location
+  if node.nodeType == Node.TEXT_NODE
+    return [node, location]
+  else if node.tagName == "BR"
+    return [node, location]
   else
-    toMinus = 0
-    for childNode in rootNode.childNodes
-      [retNod, retOff] = nodeAndOffsetFromCursorLocation(cursorLocation, childNode)
-      if retOff and !retNod
-        cursorLocation -= retOff
-        toMinus += retOff
-      else if retOff and retNod
-        return [retNod, retOff]
-  [null, toMinus]
+    for childNode in node.childNodes
+      if lengthOfNode(childNode) < location
+        location -= lengthOfNode(childNode)
+      else
+        return nodeAndOffsetFromCursorLocation(location, childNode)

 saveCursorLocation = (jObject) ->
   plainObject = jObject[0]
@@ -230,7 +240,7 @@
       div.css("display", "inline-block")
     area.val(div.data("content"))

-  div.on "keyup paste", ->
+  div.on "input paste", ->
     saveCursorLocation(div)
     div.data("content", formattedTextToDataText(div.html())).trigger("send-content").trigger("receive-content")


allow to have multiple start panels

allow to have multiple start panels

Instead of passing `options.startPanel` you can pass in
`options.startPanels` with an array of panels to open.

--- src/javascripts/theme.coffee
+++ src/javascripts/theme.coffee
@@ -139,7 +139,7 @@

     @buttons = @elem.find('.buttons')
     @panels = @elem.find('.panels')
-    @panels.hide() unless @app.isInIframeMode()
+    @panels.hide() unless @app.isInIframeMode() || @app.options.startPanels

   bindCoverLoad: =>
     @coverImage.on 'load', =>
@@ -170,6 +170,9 @@

     if extension.name() == @app.options.startPanel
       extension.button.trigger('click')
+
+    if @app.options.startPanels && @app.options.startPanels.indexOf(extension.name()) != -1
+      extension.panel.show()

     if !@app.options.startPanel && @app.isInIframeMode()
       @buttons.hide()


clean up theme loading and rendering

clean up theme loading and rendering

--- src/javascripts/theme.coffee
+++ src/javascripts/theme.coffee
@@ -6,12 +6,9 @@
 class Theme
   constructor: (@app) ->
     @context = @app.episode
-    @themeName = @app.options.theme
-    @html = @app.options.themeHtml
-    @css = @app.options.themeCss
-    @loadHtml()
-    @loadCss()
+    @loadThemeFiles()

+  html: null
   render: =>
     @elem = $(@html)
     rivets.bind(@elem, @context)
@@ -23,38 +20,35 @@

     return @elem

-  loadHtml: () =>
+  loadThemeFiles: () =>
+    theme = @app.options.theme || 'default'
+    if theme.constructor == String
+      @loadInternalTheme(theme)
+    else
+      @loadCss(theme.css)
+      @loadHtml(theme.html)
+
+  loadInternalTheme: (name) =>
+    pathPrefix = "themes/#{name}/index"
+    @loadCss("#{pathPrefix}.css")
+    @loadHtml("#{pathPrefix}.html")
+
+  loadHtml: (path) =>
     loaded = $.Deferred()
+    self = this

-    self = this
-    @html ?= "themes/#{@themeName}/index.html"
-
-    if @html.match('^.*\.html$')
-      $.get(@html).done (html) =>
-        self.html = html
-        loaded.resolve()
-      @html = null
-    else
-      @html ?= @defaultHtml
+    $.get(path).done (html) =>
+      self.html = html
       loaded.resolve()

     @loaded = loaded.promise()

-  loadCss: =>
-    @css ?= "themes/#{@themeName}/index.css"
-
-    if @css.match('^.*\.css')
-      style = $('<link>').attr
-        href: @css
-        rel: 'stylesheet'
-        type: 'text/css'
-        media: 'all'
-    else
-      style = $('<style>').attr
-        type: 'text/css'
-        media: 'all'
-      style.append(@css)
-
+  loadCss: (path) =>
+    style = $('<link>').attr
+      href: path
+      rel: 'stylesheet'
+      type: 'text/css'
+      media: 'all'
     $('head').append(style)

   addEmbedModeClass: ->


Set main export to $

Set main export to $

--- src/jquery-extensions.coffee
+++ src/jquery-extensions.coffee
@@ -142,4 +142,7 @@
 $.Event.prototype.currentTargetView = -> $(this.currentTarget).view()
 $.Event.prototype.targetView = -> $(this.target).view()

-(exports ? this).$ = $
+if module?
+  module.exports = $
+else
+  this.$ = $


startsWith isn't JS

startsWith isn't JS

whoops!

--- core/app/assets/javascripts/local_storage_polyfill.coffee
+++ core/app/assets/javascripts/local_storage_polyfill.coffee
@@ -26,7 +26,7 @@
 bad_keys = []
 for i in [0...safeLocalStorage.length]
   key = safeLocalStorage.key i
-  if key.startsWith('add_comment_to_fact_') && !safeLocalStorage.getItem key
+  if !key.lastIndexOf('add_comment_to_fact_',0) && !safeLocalStorage.getItem key
     bad_keys.push key

 for key in bad_keys


Removed useless object structure

Removed useless object structure

--- core/app/backbone/views/wheel/base_fact_wheel_view.coffee
+++ core/app/backbone/views/wheel/base_fact_wheel_view.coffee
@@ -7,14 +7,11 @@
     radius: 16
     minimalVisiblePercentage: 15

-    defaultStroke:
-      opacity: 0.2
-
-    hoverStroke:
-      opacity: 0.5
-
-    userOpinionStroke:
-      opacity: 1.0
+    defaultStrokeOpacity: 0.2
+
+    hoverStrokeOpacity: 0.5
+
+    userOpinionStrokeOpacity: 1.0

     opinionStyles:
       believe:
@@ -80,7 +77,7 @@
     @randomActions()

   createOrAnimateArc: (opinionType, percentageOffset) ->
-    opacity = (if opinionType.is_user_opinion then @options.userOpinionStroke.opacity else @options.defaultStroke.opacity)
+    opacity = (if opinionType.is_user_opinion then @options.userOpinionStrokeOpacity else @options.defaultStrokeOpacity)

     # Our custom Raphael arc attribute
     arc = [opinionType.displayPercentage, percentageOffset, @options.radius]
@@ -153,16 +150,16 @@
       path: [["M", start.x, start.y], ["A", radius, radius, 0, direction, 0, end.x, end.y]]

   mouseoverOpinionType: (path, opinionType) ->
-    destinationOpacity = @options.hoverStroke.opacity
-    destinationOpacity = @options.userOpinionStroke.opacity  if opinionType.is_user_opinion
+    destinationOpacity = @options.hoverStrokeOpacity
+    destinationOpacity = @options.userOpinionStrokeOpacity  if opinionType.is_user_opinion
     path.animate(
       "stroke-width": @hoverStrokeWidth()
       opacity: destinationOpacity
     , 200, "<>")

   mouseoutOpinionType: (path, opinionType) ->
-    destinationOpacity = @options.defaultStroke.opacity
-    destinationOpacity = @options.userOpinionStroke.opacity  if opinionType.is_user_opinion
+    destinationOpacity = @options.defaultStrokeOpacity
+    destinationOpacity = @options.userOpinionStrokeOpacity  if opinionType.is_user_opinion
     path.animate(
       "stroke-width": @defaultStrokeWidth()
       opacity: destinationOpacity


Remove old documentation

Remove old documentation

--- src/kissmetrics-batch.coffee
+++ src/kissmetrics-batch.coffee
@@ -38,12 +38,6 @@
   # `data` (Object): Key/value pairs of Kissmetrics properties. Some
   #   properties will be renamed in `_transformData()` based on `data.type`
   #   due to differences between Kissmetrics' batch API and regular HTTP API.
-  #
-  # `timestamp` Optional (Integer): Unix timestamp from the time the event
-  #   occurred. Defaults to the current time. This is intentionally not set
-  #   as a default argument because it can also be set by transforming the
-  #   `_t` property.
-
   #
   # ```
   # batch.add({name: 'Evan', home: 'San Francisco'}, 482698020);


Support Kissmetrics' timestamp property in batched queries

--- src/kissmetrics-batch.coffee
+++ src/kissmetrics-batch.coffee
@@ -46,9 +46,9 @@
   # batch.add({name: 'Evan', home: 'San Francisco'}, 482698020);
   # ```

-  add: (data, timestamp = Math.round((new Date).getTime() / 1000)) ->
-    data.timestamp = timestamp
+  add: (data, timestamp) ->
     @_transformData data
+    data.timestamp ?= Math.round((new Date).getTime() / 1000)

     @queue.add data

@@ -134,6 +134,7 @@
   # HTTP API.
   #
   # * `_p` (person) is replaced by `identity`
+  # * `_t` (timestamp) is replaced by `timestamp`
   # * `record` queries use the `event` property instead of `_n`
   # * `alias` queries use the `alias` property instead of `_n`
   # * `_k` (API key) is replaced by an HTTP header
@@ -145,6 +146,7 @@

   _transformData: (data) ->
     data.identity = data._p
+    data.timestamp = data._t if data._t

     switch data.type
       when 'record' then data.event = data._n
@@ -153,6 +155,7 @@
     delete data._k
     delete data._n
     delete data._p
+    delete data._t
     delete data.type


加入分页功能

加入分页功能

--- biz/issue.coffee
+++ biz/issue.coffee
@@ -95,19 +95,37 @@
     2. 有comment的issue
   ###
   getProjectDiscussion: (req, res, next)->
+    self = @
     project_id = req.params.project_id
     limit = req.query.limit || 10
     offset = req.query.offset || 0
+    sql = "SELECT :fields FROM issue WHERE project_id = #{project_id}
+      AND (tag = 'project' OR id in
+      (SELECT issue_id FROM comment WHERE project_id = #{project_id}))"

-    sql = "SELECT * FROM issue WHERE project_id = #{project_id}
-      AND (tag = 'project' OR id in
-      (SELECT issue_id FROM comment WHERE project_id = #{project_id}))
-      ORDER BY timestamp collate nocase DESC limit #{limit} offset #{offset}"
+    queue = []

-    this.entity().knex.raw(sql).then (result)->
+    queue.push(
+      (done)->
+        self.scalar sql.replace(':fields', 'count(id)'), done
+    )
+
+    queue.push(
+      (done)->
+        sql += "ORDER BY timestamp collate nocase DESC limit #{limit} offset #{offset}"
+        sql = sql.replace ':fields', '*, (SELECT realname FROM member WHERE member.id = issue.creator) AS realname'
+        self.entity().knex.raw(sql).then (result)-> done null, result[0]
+    )
+
+    _async.series queue, (err, result)->
       data =
-        items: result[0]
+        items: result[1]
+        pagination:
+          limit: limit,
+          offset: offset
+          count: result[0]
       res.json data
+
     #http://127.0.0.1:14318/api/project/1/discussion

 module.exports = Issue


add some methods to WRITE ACL

add some methods to WRITE ACL

--- src/lib/acl-generator.coffee
+++ src/lib/acl-generator.coffee
@@ -83,7 +83,7 @@
                 @addDenyACL('$owner', ['WRITE'], @getRestrictingProperties(relationDefine.type, 'WRITE', roleName))

         for roleName, accessTypes of @aclConditions.customPermissions
-            @addAllowACL(roleName, accessTypes, ['create', 'updateAttributes'])
+            @addAllowACL(roleName, accessTypes, ['create', 'updateAttributes', 'upsert', 'deleteById'])

         return @acl


JStack: allow modify to set credentials but only supported ones and make sure they are unique

--- workers/social/lib/social/models/stack.coffee
+++ workers/social/lib/social/models/stack.coffee
@@ -8,8 +8,11 @@

   { secure, ObjectId, signature, daisy } = require 'bongo'
   { Relationship }   = jraphical
+  { uniq }           = require 'underscore'
   { permit }         = require './group/permissionset'
   Validators         = require './group/validators'
+  { PROVIDERS }      = require './computeproviders/computeutils'
+

   @trait __dirname, '../traits/protected'

@@ -291,6 +294,7 @@
         @remove callback

+  SUPPORTED_CREDS = (Object.keys PROVIDERS).concat ['userInput', 'custom']

   modify: permit

@@ -300,15 +304,27 @@

     success: (client, options, callback) ->

-      { title, config } = options
-
-      unless title or config
+      { title, config, credentials } = options
+
+      unless title or config or credentials
         return callback new KodingError 'Nothing to update'

-      title  ?= @title
-      config ?= @config
-
-      @update $set : { title, config }, (err) ->
+      dataToUpdate        = {}
+      dataToUpdate.title  = title   if title?
+      dataToUpdate.config = config  if config?
+
+      if credentials?
+        unless typeof credentials is 'object'
+          return callback new KodingError 'Credential should be an Object'
+
+        sanitized = {}
+        for key, value of credentials
+          if key in SUPPORTED_CREDS
+            sanitized[key] = uniq value
+
+        dataToUpdate.credentials = sanitized
+
+      @update $set : dataToUpdate, (err) ->
         return callback err  if err?
         callback null


fixed bug when dragging images from the middle downwards

--- plugins/image.coffee
+++ plugins/image.coffee
@@ -208,13 +208,13 @@
                     timer = setTimeout(functionToCall, delay)  unless timer

                 # Calculate position on an initial drag
-                calcPosition: (ui, offset) ->
+                calcPosition: (ui, offset, event) ->
                     position = offset.left + third
-                    if ui.offset.left >= position and ui.offset.left <= (offset.left + third * 2)
+                    if event.pageX >= position and event.pageX <= (offset.left + third * 2)
                         "middle"
-                    else if ui.offset.left < position
+                    else if event.pageX < position
                         "left"
-                    else "right"  if ui.offset.left > (offset.left + third * 2)
+                    else "right"  if event.pageX > (offset.left + third * 2)

                 # removes all temporary nodes created before
                 removeTmpNodes: ->
@@ -272,9 +272,7 @@
             dnd =
                 handleDragEvent: (event, ui) ->
                     tmpObject = $(".tmp", editable)
-                    internalDrop = helper.checkOrigin(event)
-                    position = helper.calcPosition(ui, offset)
-                    $(event.target).remove() if internalDrop
+                    position = helper.calcPosition(ui, offset, event)

                     if position is "middle"
                         if tmpObject.parent("div").length is 0
@@ -287,6 +285,14 @@
                     helper.showOverlay position

                 handleStartEvent: (event, ui) ->
+                    internalDrop = helper.checkOrigin(event)
+                    $(event.target).css('display','none') if internalDrop
+
+                    $.ui.ddmanager.prepareOffsets(
+                        $(event.target).data('draggable'), event
+                    )
+
+                    $(event.target).remove() if internalDrop
                     $(document).trigger('startPreventSave');

                 handleStopEvent: (event, ui) ->
@@ -306,7 +312,7 @@
                     # check whether it is an internal drop or not
                     internalDrop = helper.checkOrigin(event)
                     helper.removeTmpNodes()
-                    position = helper.calcPosition(ui, offset)
+                    position = helper.calcPosition(ui, offset, event)
                     imageInsert = helper.createInsertElement(ui, false)

                     if position is "middle"
@@ -339,7 +345,7 @@
                         editable.append overlay.right

                         helper.removeTmpNodes()
-                        position = helper.calcPosition(ui, offset)
+                        position = helper.calcPosition(ui, offset, event)

                         createTmp = ->
                             if position is "middle"


no internet

no internet

--- Framework/MobileAppBase.coffee
+++ Framework/MobileAppBase.coffee
@@ -19,7 +19,7 @@
     @xhr = Ti.Network.createHTTPClient({ timeout: 15000 })
     @includedFiles = []

-    Ti.Network.addEventListener('change', (e) => @checkInternet())
+    Ti.Network.addEventListener('change', (e) => @checkInternet() if !@checking)

   delay: (ms, func) -> setTimeout func, ms
   create: (className, options = {}) -> @classFactory.create(className, options)
@@ -37,21 +37,22 @@

   noInternetEnable: =>
     @settings.noInternetViewEnabled = true
-    if !@noInternetView?
-      @noInternetView = @create("NoInternetView")
-
-    @checkInternet()
+    @checkInternet() if !@checking

   noInternetDisable: =>
     @settings.noInternetViewEnabled = false
-    @checkInternet()
+    @checkInternet() if !@checking

   checkInternet: =>
+    @checking = true
+
     if Ti.Network.online
       Ti.API.info('Welcome to the internet')
-      if @noInternetView?
-        @noInternetView.window.close()
+      @noInternetView = @create("NoInternetView") unless @noInternetView
+      @noInternetView.window.close()
     else
       Ti.API.info('Bloody hell, the network just DISSAPEARED!')
       if @noInternetView? && @settings.noInternetViewEnabled
-        @noInternetView.window.open()+        @noInternetView.window.open()
+
+    @checking = false


cleanup usage queue time coffee

cleanup usage queue time coffee

--- js/inner/build.coffee
+++ js/inner/build.coffee
@@ -259,10 +259,16 @@
       (@run_queued_time() || 0) + (@usage_queued_time() || 0)

     @queued_time_summary = @komp =>
-      if @run_queued_time()
-        "#{CI.time.as_duration(@usage_queued_time())} waiting + #{CI.time.as_duration(@run_queued_time())} in queue"
+      time = CI.time.as_duration
+      use = @usage_queued_time()
+      run = @run_queued_time()
+      if run
+        if use < 1000
+          "#{time(run)}"
+        else
+          "#{time(use)} waiting, #{time(run)} queued"
       else
-        "#{CI.time.as_duration(@usage_queued_time())} waiting for builds to finish"
+        "#{time(use)} waiting for builds to finish"

     @branch_in_words = @komp =>
       return "unknown" unless @branch()


arrow pointer no longer covers star name

--- src/game/states/game.coffee
+++ src/game/states/game.coffee
@@ -3,8 +3,8 @@
     name: 'The Little Dipper',
     numStars: 7,
     startingStar: 0,
-    arrowLocX:97,
-    arrowLocY:168,
+    arrowLocX:106,
+    arrowLocY:160,
     starsArray: [
         {x: 309, y: 40},
         {x: 257, y: 125},
@@ -18,8 +18,8 @@
     name: 'The Big Dipper',
     numStars: 7,
     startingStar: 0,
-    arrowLocX:261,
-    arrowLocY:187,
+    arrowLocX:285,
+    arrowLocY:180,
     starsArray: [
         {x: 320, y: 95},
         {x: 411, y: 98},
@@ -34,8 +34,8 @@
     name: 'Boötes',
     numStars: 6,
     startingStar: 0,
-    arrowLocX:681,
-    arrowLocY:446,
+    arrowLocX:420,
+    arrowLocY:490,
     starsArray: [
         {x: 586, y: 299},
         {x: 676, y: 226},
@@ -49,8 +49,8 @@
     name: 'Virgo',
     numStars: 9,
     startingStar: 0,
-    arrowLocX:586,
-    arrowLocY:481,
+    arrowLocX:645,
+    arrowLocY:529,
     starsArray: [
         {x: 380, y: 372},
         {x: 289, y: 509},
@@ -69,8 +69,8 @@
     name: 'Leo',
     numStars: 9,
     startingStar: 0,
-    arrowLocX:581,
-    arrowLocY:156,
+    arrowLocX:560,
+    arrowLocY:170,
     starsArray: [
         {x: 525, y: 180},
         {x: 410, y: 392},
@@ -201,6 +201,7 @@
       @map.alpha = 0
       @backImage.alpha = 0
       @game.add.tween(@map).to({alpha:1},1000,Phaser.Easing.Linear.Out,true)
-      @game.add.sprite @level.arrowLocX, @level.arrowLocY, 'arrow'
+      @hereArrow = @game.add.sprite @level.arrowLocX, @level.arrowLocY, 'arrow'
+      @hereArrow.anchor.setTo 0.5, 1

 module.exports = Game


sentence

sentence

--- generators/app/templates/scripts/example.coffee
+++ generators/app/templates/scripts/example.coffee
@@ -4,7 +4,7 @@
 # Notes:
 #   They are commented out by default, because most of them are pretty silly and
 #   wouldn't be useful and amusing enough for day to day huboting.
-#   Uncomment the ones you want to try and experiment with
+#   Uncomment the ones you want to try and experiment with.
 #
 #   These are from the scripting documentation: https://github.com/github/hubot/blob/master/docs/scripting.md


fixes calculate question label not required bug

--- jsapp/xlform_model_view/model.base.coffee
+++ jsapp/xlform_model_view/model.base.coffee
@@ -94,6 +94,9 @@
         return value:
           unique: true
           required: true
+      else if @key == 'calculation'
+        return value:
+          required: true
       else if @key == 'label' && @_parent.constructor.key != 'group'
         return value:
           required: true


close the quotes

close the quotes

--- src/hubot-confluence.coffee
+++ src/hubot-confluence.coffee
@@ -100,7 +100,7 @@
   host = nconf.get("HUBOT_CONFLUENCE_HOST")
   port = nconf.get("HUBOT_CONFLUENCE_PORT")
   protocol = nconf.get("HUBOT_CONFLUENCE_PROTOCOL")
-  context_root = nconf.get("HUBOT_CONFLUENCE_CONTEXT_ROOT)
+  context_root = nconf.get("HUBOT_CONFLUENCE_CONTEXT_ROOT")

   url = "#{protocol}://#{host}:#{port}/#{context_root}"
   # account for an empty context root


Get spec to run

Get spec to run

--- spec/tree-view-package-spec.coffee
+++ spec/tree-view-package-spec.coffee
@@ -4483,24 +4483,24 @@
         expect(atom.project.getPaths()).toEqual [alphaDirPath, thetaDirPath]
         expect(document.querySelector('.placeholder')).not.toExist()

-  describe "When a file is opened", ->
-    describe "if the file entry does not exists in tree view", ->
-      it "deselects existing file entry", ->
-        newfilepath = path.join(os.tmpdir(), 'new-file.txt')
-        fs.writeFileSync(newfilepath, 'test')
-
-        waitsForFileToOpen ->
-          sampleJs.click()
+  describe "when a file is opened", ->
+    describe "if the file is not in a currently opened project", ->
+      it "deselects all entries", ->
+        nonProjectPath = path.join(os.tmpdir(), 'new-file.txt')
+        fs.writeFileSync(nonProjectPath, 'test')
+
+        waitForWorkspaceOpenEvent ->
+          sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1}))

         runs ->
           expect(sampleJs).toHaveClass 'selected'
-          expect(treeView.find('.selected').length).toBe 1
+          expect(treeView.getSelectedEntries().length).toBe 1

         waitsForPromise ->
-          atom.workspace.open(newfilepath)
-
-        runs ->
-          expect(treeView.find('.selected').length).toBe 0
+          atom.workspace.open(nonProjectPath)
+
+        runs ->
+          expect(treeView.getSelectedEntries().length).toBe 0

   describe "when there is a __proto__ entry present", ->
     it "does not break anything", ->


fixed parseFloat and parseInt

fixed parseFloat and parseInt

--- src/compiler/tokenizer.coffee
+++ src/compiler/tokenizer.coffee
@@ -383,7 +383,7 @@
           ++count
         if @is_whitespace(@next())
           @consume_next()
-        number = Number.parseInt(digits.join(''),16)
+        number = parseInt(digits.join(''),16)
         if number is 0 or number in [MIN_SURROGATE_CODE_POINT..MAX_SURROGATE_CODE_POINT] or number > MAX_ALLOWED_CODE_POINT
           return "\ufffd"

@@ -490,7 +490,7 @@

   string_to_number: (s) ->
-    Number.parseFloat(s)
+    parseFloat(s)

   consume_the_remnants_of_a_bad_url: () ->
     while true


detect if assets are added as well

detect if assets are added as well

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -116,7 +116,8 @@
   (node.src || node.href) for node in doc.head.childNodes when node.src or node.href

 assetsChanged = (doc)->
-  intersection(extractAssets(doc), assets).length != assets.length
+  extractedAssets = extractAssets doc
+  extractedAssets.length isnt assets.length or intersection(extractedAssets, assets).length != assets.length

 intersection = (a, b) ->
   [a, b] = [b, a] if a.length > b.length


Remove hash from XHR url - Fix #142

Remove hash from XHR url - Fix #142

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -18,7 +18,7 @@
   triggerEvent 'page:fetch'

   xhr = new XMLHttpRequest
-  xhr.open 'GET', url, true
+  xhr.open 'GET', removeHash(url), true
   xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
   xhr.setRequestHeader 'X-XHR-Referer', referer

@@ -109,6 +109,13 @@
 resetScrollPosition = ->
   window.scrollTo 0, 0

+removeHash = (url) ->
+  link = url
+  unless url.href?
+    link = document.createElement 'A'
+    link.href = url
+  link.href.replace link.hash, ''
+

 triggerEvent = (name) ->
   event = document.createEvent 'Events'
@@ -174,7 +181,7 @@
   location.protocol isnt link.protocol or location.host isnt link.host

 anchoredLink = (link) ->
-  ((link.hash and link.href.replace(link.hash, '')) is location.href.replace(location.hash, '')) or
+  ((link.hash and removeHash(link)) is removeHash(location)) or
     (link.href is location.href + '#')

 nonHtmlLink = (link) ->


code reduction and clarification

code reduction and clarification

--- Smooth.coffee
+++ Smooth.coffee
@@ -69,18 +69,14 @@
 		@array = array.slice 0 #copy the array
 		@length = @array.length #cache length

-
-		clipHelpers =
+		#Set the clipping helper method
+		throw "Invalid clip: #{config.clip}" unless @clipHelper = {
 			clamp: @clipHelperClamp
 			zero: @clipHelperZero
 			periodic: @clipHelperPeriodic
 			mirror: @clipHelperMirror
-
-		#Set the clipping helper method
-		@clipHelper = clipHelpers[config.clip]
-
-		throw "Invalid clip: #{config.clip}" unless @clipHelper?
-
+		}[config.clip]
+

     # Get input array value at i, applying the clipping method
 	getClippedInput: (i) ->
@@ -110,15 +106,14 @@
 class LinearInterpolator extends AbstractInterpolator
 	interpolate: (t) ->
 		k = Math.floor t
-		a = @getClippedInput k
-		b = @getClippedInput k+1
 		#Translate t to interpolate between k and k+1
 		t -= k
-		return (1-t)*a + (t)*b
+		return (1-t)*@getClippedInput(k) + (t)*@getClippedInput(k+1)

 class CubicInterpolator extends AbstractInterpolator
 	constructor: (array, config)->
+		#clamp cubic tension to [0,1] range
 		@tangentFactor = 1 - Math.max 0, Math.min 1, config.cubicTension
 		super

@@ -137,15 +132,14 @@
 		return (2*t3 - 3*t2 + 1)*p[0] + (t3 - 2*t2 + t)*m[0] + (-2*t3 + 3*t2)*p[1] + (t3 - t2)*m[1]

 {sin, PI} = Math
-sinc = (x) ->
-	if x is 0 then 1
-	else sin(PI*x)/(PI*x)
-
-makeLanczosWindow = (a) ->
-	(x) -> sinc(x/a) #lanczos window
-
-makeSincKernel = (window) ->
-	(x) -> sinc(x)*window(x)
+#Normalized sinc function
+sinc = (x) -> if x is 0 then 1 else sin(PI*x)/(PI*x)
+
+#Make a lanczos window function for a given filter size 'a'
+makeLanczosWindow = (a) -> (x) -> sinc(x/a)
+
+#Make a sinc kernel function by multiplying the sinc function by a window function
+makeSincKernel = (window) -> (x) -> sinc(x)*window(x)

 class SincFilterInterpolator extends AbstractInterpolator
 	constructor: (array, config) ->
@@ -153,16 +147,16 @@
 		#Create the lanczos kernel function
 		@a = config.sincFilterSize

-		window = config.sincWindow
-		throw 'No sincWindow provided' unless window?
-		@kernel = makeSincKernel window
+		#Cannot make sinc filter without a window function
+		throw 'No sincWindow provided' unless config.sincWindow
+		#Window the sinc function to make the kernel
+		@kernel = makeSincKernel config.sincWindow

 	interpolate: (t) ->
 		k = Math.floor t
 		#Convolve with Lanczos kernel
 		sum = 0
-		for n in [(k - @a + 1)..(k + @a)]
-			sum += @kernel(t - n)*@getClippedInput(n)
+		sum += @kernel(t - n)*@getClippedInput(n) for n in [(k - @a + 1)..(k + @a)]
 		sum

@@ -194,6 +188,7 @@
 	throw 'Non-vector in Smooth() input' unless getType(v) is 'Array'
 	throw 'Inconsistent dimension in Smooth() input' unless v.length is dimension
 	validateNumber n for n in v
+	return

 isValidNumber = (n) -> (getType(n) is 'Number') and isFinite(n) and not isNaN(n)

@@ -280,21 +275,15 @@

 			else throw "Invalid element type: #{dataType}"

-	if config.scaleTo
-		scaleRange = normalizeScaleTo config.scaleTo
-		#Because periodic functions repeat, we scale the domain to extend to the beginning of the next cycle.
-		if config.clip is Smooth.CLIP_PERIODIC
-			baseScale = arr.length
-		else #for other clipping types, we scale the domain to extend exactly to the end of the input array
-			baseScale = arr.length - 1
-		smoothFunc = makeScaledFunction smoothFunc, baseScale, scaleRange
-		properties.domain = [scaleRange...].sort() #make sure domain interval is unflipped
-	else
-		#standard domain is from 0 to end of array
-		properties.domain = [0, arr.length-1]
-		#extend domain by 1 for periodic functions
-		properties.domain[1]++ if config.clip is Smooth.CLIP_PERIODIC
-
+	# Determine the end of the original function's domain
+	if config.clip is 'periodic' then baseDomainEnd = arr.length #after last element for periodic
+	else baseDomainEnd = arr.length - 1 #at last element for non-periodic
+
+	config.scaleTo ||= baseDomainEnd #default scales to the end of the original domain for no effect
+
+	properties.domain = normalizeScaleTo config.scaleTo
+	smoothFunc = makeScaledFunction smoothFunc, baseDomainEnd, properties.domain
+	properties.domain.sort()

 	###copy properties###
 	smoothFunc[k] = v for own k,v of properties


Omit garbage

Omit garbage


PR #123: Fixed unit tests

PR #123: Fixed unit tests


FinderController: simplfy the ::reset and ::ready

--- client/Finder/filetree/controllers/findercontroller.coffee
+++ client/Finder/filetree/controllers/findercontroller.coffee
@@ -72,30 +72,16 @@
     @reset()  if @getOptions().loadFilesOnInit

   reset:->
-    KD.singletons.vmController.ready =>
-      if @getOptions().useStorage
-        @appStorage.ready => @loadVms()
-      else
-        @utils.defer => @loadVms()
-
-  parseSavedVms = (vms) ->
-    vms.reduce (memo, str) ->
-      [vmName, path] = str.split ':'
-      memo[0].push vmName
-      memo[1].push path
-      memo
-    , [[],[]]
-
-  fetchSavedVms: (savedVms, callback) ->
-    [vmNames, paths] = parseSavedVms savedVms
-
-    KD.getSingleton('vmController').fetchVmsByName vmNames, (err, vms) =>
-      return callback? err  if err
-
-      vms[i].path = paths[i]  for _, i in vms
+
+    KD.singletons.computeController.ready =>
+
+      if @getOption 'useStorage'
+      then @appStorage.ready @bound 'loadMachines'
+      else @utils.defer      @bound 'loadMachines'
+
+
   loadMachines:->

-      callback null, vms
     { computeController } = KD.singletons

     computeController.fetchMachines (err, machines)=>


Cleanup messed code

Cleanup messed code

--- client/app/CommonViews/comments/commentlistitemview.coffee
+++ client/app/CommonViews/comments/commentlistitemview.coffee
@@ -12,36 +12,22 @@
     originType  = data.getAt('originType')
     deleterId   = data.getAt('deletedBy')?.getId?()

-    origin = {
+    origin =
       constructorName  : originType
       id               : originId
-    }
+
     @avatar = new AvatarView {
-      size    : {width: 30, height: 30}
-      origin
-    }
-    @author = new ProfileLinkView {
+      size     :
+        width  : 30
+        height : 30
       origin
     }

+    @author = new ProfileLinkView { origin }
+
     if deleterId? and deleterId isnt originId
       @deleter = new ProfileLinkView {}, data.getAt('deletedBy')
-    # if data.originId is KD.whoami().getId()
-    #   @settingsButton = new KDButtonViewWithMenu
-    #     style       : 'transparent activity-settings-context'
-    #     cssClass    : 'activity-settings-menu'
-    #     title       : ''
-    #     icon        : yes
-    #     delegate    : @
-    #     iconClass   : "cog"
-    #     menu        : [
-    #       type      : "contextmenu"
-    #       items     : [
-    #         { title : 'Delete', id : 2,  parentId : null, callback : => data.delete (err)=> @propagateEvent KDEventType: 'CommentIsDeleted' }
-    #       ]
-    #     ]
-    #     callback    : (event)=> @settingsButton.contextMenu event
-    # else
+
     @deleteLink = new KDCustomHTMLView
       tagName     : 'a'
       attributes  :


Decode HTML entities to edit machine label

--- client/app/lib/providers/machinesettingsgeneralview.coffee
+++ client/app/lib/providers/machinesettingsgeneralview.coffee
@@ -124,7 +124,7 @@
         nickEdit.hide()
         nickname.show()

-      nickEdit.setValue @machine.label
+      nickEdit.setValue Encoder.htmlDecode @machine.label
       nickEdit.show()

       kd.utils.defer -> nickEdit.setFocus()


Link to issues

Link to issues

--- src/browser/squirrel-update.coffee
+++ src/browser/squirrel-update.coffee
@@ -88,7 +88,8 @@
   spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) ->
     if error?
       if error.code is 1
-        # Don't overwrite path when reading value is disabled
+        # FIXME Don't overwrite path when reading value is disabled
+        # https://github.com/atom/atom/issues/5092
         if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1
           return callback(error)

@@ -110,6 +111,8 @@
       if isAscii(pathEnv)
         callback(null, pathEnv)
       else
+        # FIXME Don't corrupt non-ASCII PATH values
+        # https://github.com/atom/atom/issues/5063
         callback(new Error('PATH contains non-ASCII values'))
     else
       callback(new Error('Registry query for PATH failed'))


Fix f'img

Fix f'img

--- modules/image.coffee
+++ modules/image.coffee
@@ -35,14 +35,14 @@
           msg.reply '', false, {
             title: '[click for sauce]'
             url: r.items[0].image.contextLink
-            image: { r.items[0].link }
+            image: { url: r.items[0].link }
           }
         else
           image = @chance.pickone r.items
           msg.reply '', false, {
             title: '[click for sauce]'
             url: image.image.contextLink
-            image: { image.link }
+            image: { url: image.link }
           }
       .catch (err)=>
         return msg.reply 'Daily limit exceeded.' if err.statusCode is 403


Simplified callback

Simplified callback

--- core/app/backbone/views/buttons/react_delete_button.coffee
+++ core/app/backbone/views/buttons/react_delete_button.coffee
@@ -8,12 +8,10 @@

   _slideButtonIn: -> @setState opened: false

-  _onDelete: -> @props.onDelete()
-
   render: ->
     second_button =
       R.span className: "delete-button-second-container",
-        R.span className: "delete-button-second button-small button-danger", onClick: @_onDelete,
+        R.span className: "delete-button-second button-small button-danger", onClick: @props.onDelete,
           @props.text || 'Delete'
         R.span className: "delete-button-arrow"


Fix error when we have a transfer link

Fix error when we have a transfer link

--- src/code/stores/graph-store.coffee
+++ src/code/stores/graph-store.coffee
@@ -561,7 +561,7 @@

       if link.relation?.formula
         relation = RelationFactory.selectionsFromRelation(link.relation)
-        if relation.scalar.id isnt "aboutTheSame"
+        if relation.scalar and relation.scalar.id isnt "aboutTheSame"
           return AppSettingsStore.store.Complexity.expanded

     return AppSettingsStore.store.Complexity.basic


correct errors to error on handle call

correct errors to error on handle call

--- src/coffee/services/validation.coffee
+++ src/coffee/services/validation.coffee
@@ -32,7 +32,7 @@
             if validators[rule.validator] and validators[rule.validator].validator?
               if validators[rule.validator].validator(fieldName, values, rule) is false
                 errors[fieldName] = rule.options.message
-                events.handle(fieldName, "errors",rule.options.message)
+                events.handle(fieldName, "error",rule.options.message)
                 return false
         removeError(fieldName)
         return true


Fixed autocomplete on calling onSuccess and onError properly

--- src/directives/autocomplete.coffee
+++ src/directives/autocomplete.coffee
@@ -11,6 +11,7 @@
 @param {String} ng-model Assignable angular expression to data-bind to
 @param {String} mac-placeholder Placeholder text
 @param {String} mac-autocomplete-url Url to fetch autocomplete dropdown list data
+@param {Expression} mac-autocomplete-source Local data source
 @param {Boolean} mac-autocomplete-disabled Boolean value if autocomplete should be disabled
 @param {Function} mac-autocomplete-on-select Function called when user select on an item
        - `selected` - {Object} The item selected
@@ -102,12 +103,12 @@

           $http(options)
             .success (data, status, headers, config) ->
-                fetchedList  = onSuccess?({data, status, headers})
-                fetchedList ?= data.data
+              fetchedList  = onSuccess? $scope, {data, status, headers}
+              fetchedList ?= data.data

-                resp updateList fetchedList
+              resp updateList fetchedList
             .error (data, status, headers, config) ->
-              onError? {data, status, headers}
+              onError? $scope, {data, status, headers}
         else
           list = updateList(source($scope) or [])
           resp $filter("filter") list, req.term
@@ -119,7 +120,8 @@

         select: (event, ui) ->
           $scope.$apply ->
-            selected = _(currentAutocomplete).find (item) -> item[labelKey] is ui.item.label
+            selected = _(currentAutocomplete).find (item) ->
+              (item[labelKey] or item) is ui.item.label
             onSelect $scope, {selected} if onSelect?

       if attrs.macAutocompleteDisabled?


lookupContentType: Match known Content-Types with or without +xml suffix.

--- lib/index.coffee
+++ lib/index.coffee
@@ -509,11 +509,14 @@
     if contentType of AssetGraph.typeByContentType
       AssetGraph.typeByContentType[contentType]
     else if /\+xml$/i.test(contentType)
-      "Xml"
+      contentTypeWithoutXmlSuffix = contentType.replace(/\+xml$/i, '')
+      AssetGraph.typeByContentType[contentTypeWithoutXmlSuffix] or 'Xml'
+    else if ("#{contentType}+xml") of AssetGraph.typeByContentType
+      AssetGraph.typeByContentType["#{contentType}+xml"]
     else if /^text\//i.test(contentType)
-      "Text"
-    else
-      "Asset"
+      'Text'
+    else
+      'Asset'

 AssetGraph.createAsset = AssetGraph::createAsset = (assetConfig) ->
   unless assetConfig.type
@@ -608,4 +611,3 @@
 fs.readdirSync(Path.resolve(__dirname, 'relations')).forEach (fileName) ->
   if /\.(js|coffee)$/.test(fileName) and fileName not in ['index.js', 'index.coffee']
     AssetGraph.registerRelation Path.resolve(__dirname, 'relations', fileName)
-


Path.join paths together

Path.join paths together

--- lib/mycha.coffee
+++ lib/mycha.coffee
@@ -59,7 +59,7 @@

     # The JS/CS files to provide to Mocha.
     @file_configuration = new FileConfiguration
-      test_dir: "#{@project_directory}/#{@mycha_configuration.options.testDir}"
+      test_dir: path.join(@project_directory, @mycha_configuration.options.testDir)
       default_files: Mycha.default_files
       files: files


punt on this for now

punt on this for now

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -6,8 +6,8 @@
 #   # Triggering an incident
 #
 #   hubot who's on call - return a list of services and who is on call for them
-#   hubot who's on call for <search> - return the username of who's on call for any service matching <search>
-#   hubot pager trigger <service> <msg> - create a new incident with <msg> for whomever is on call for <service>
+#   hubot who's on call for <search> - return the username of who's on call for any schedule matching <search>
+#   hubot pager trigger <msg> - create a new incident with <msg>
 #
 #   ## Setup
 #


Update Deploy Captain abbreviation to BDC so it meets regex (#3)

--- src/scripts/pagerduty.coffee
+++ src/scripts/pagerduty.coffee
@@ -27,8 +27,8 @@
   robot.hear /who(?:’s|'s|s| is|se)? (?:(?:the )?([0-9A-Za-z]*))/i, (msg) ->
     scheduleAbbreviation = msg.match[1]
     switch scheduleAbbreviation
-      when "Deploy Captain" then getScheduleFromScheduleName(msg, 'Build & Deploy Captain')
       when "CFO" then getScheduleFromScheduleName(msg, 'Chief Frontend Officer')
+      when "BDC" then getScheduleFromScheduleName(msg, 'Build & Deploy Captain')
       else return

   getScheduleFromScheduleName = (msg, scheduleName) ->


allow array as sort parameter

allow array as sort parameter

--- lib/pages.coffee
+++ lib/pages.coffee
@@ -23,7 +23,7 @@
     router: [true, Match.Optional(String), undefined] #Can be any type. Use only in comparisons. Expects String or Boolean
     routerTemplate: [true, String, "pages"]
     routerLayout: [true, Match.Optional(String), undefined]
-    sort: [true, Object, {}]
+    sort: [true, Match.OneOf(Object, Array), {}]

     # Unavailable to the client after initialization


sidebar: improve tag description text

sidebar: improve tag description text

--- client/Social/Activity/sidebar/topicsearchmodal.coffee
+++ client/Social/Activity/sidebar/topicsearchmodal.coffee
@@ -20,6 +20,6 @@
     @addSubView new KDCustomHTMLView
       cssClass   : 'tag-description'
       partial    : "
-        You can also create a new topic by making it a part of

-        a new post. eg: I love #koding
+        You can also create a new topic by making it a part of a new post.

+        eg: I love #programming
       "


Add missing comment block

Add missing comment block

--- client/admin/lib/views/stacksv2/defaulttemplate.coffee
+++ client/admin/lib/views/stacksv2/defaulttemplate.coffee
@@ -92,3 +92,4 @@
     email:
       email: '${var.koding_username}@koding.com'
       notificationEmail: '${var.koding_user_contact_email}'
+###


Rebuild showEvents

Rebuild showEvents

--- models/mixins/relations/partner_show.coffee
+++ models/mixins/relations/partner_show.coffee
@@ -46,8 +46,9 @@
       showEvents: showEvents

   rebuild: ->
-    { artists, partner, fair, profile } = @related()
+    { artists, partner, fair, profile, showEvents } = @related()
     artists.reset @get('artists'), silent: true
     partner.set @get('partner'), silent: true
     fair.set @get('fair'), silent: true
     profile.set 'id', partner.get('default_profile_id'), silent: true
+    showEvents.reset @get('events'), silent: true


ActivitySidebar: show/hide group stacks changed warning in case of inconsistency between group and user stacks

--- client/app/lib/activity/sidebar/activitysidebar.coffee
+++ client/app/lib/activity/sidebar/activitysidebar.coffee
@@ -32,6 +32,7 @@
 ChannelActivitySideView         = require './channelactivitysideview'
 SidebarStacksNotConfiguredPopup = require 'app/activity/sidebar/sidebarstacksnotconfiguredpopup'
 isReactivityEnabled             = require 'app/util/isReactivityEnabled'
+EnvironmentsModal               = require 'app/environment/environmentsmodal'

 # this file was once nice and tidy (see https://github.com/koding/koding/blob/dd4e70d88795fe6d0ea0bfbb2ef0e4a573c08999/client/Social/Activity/sidebar/activitysidebar.coffee)
 # once we merged two sidebars into one
@@ -105,6 +106,8 @@
       .on 'MachineBeingDestroyed',     @bound 'invalidateWorkspaces'
       .on 'MachineBuilt',              @bound 'machineBuilt'
       .on 'StacksNotConfigured',       @bound 'addStacksNotConfiguredWarning'
+      .on 'GroupStacksInconsistent',   @bound 'addGroupStacksChangedWarning'
+      .on 'GroupStacksConsistent',     @bound 'hideGroupStacksChangedWarning'

     @on 'ReloadMessagesRequested',     @bound 'handleReloadMessages'
@@ -520,6 +523,41 @@
   fetchEnvironmentData: (callback) ->

     environmentDataProvider.fetch (data) => callback data
+
+
+  hideGroupStacksChangedWarning: ->
+
+    @groupStacksChangedWarning?.hide()
+
+
+  addGroupStacksChangedWarning: ->
+
+    return  if isKoding()
+
+    if @groupStacksChangedWarning and not @groupStacksChangedWarning.isDestroyed
+      return @groupStacksChangedWarning.show()
+
+    view = new KDCustomHTMLView
+      cssClass : 'warning-section'
+      partial  : """
+
+
STACKS

+
+

+          You have different resources in your stacks.
+          Please re-initialize your stacks.
+

+      """
+
+    view.addSubView new KDCustomHTMLView
+      tagName: 'a'
+      partial: 'Show Stacks'
+      click: (event) ->
+        kd.utils.stopDOMEvent event
+        new EnvironmentsModal
+
+    @groupStacksChangedWarning = \
+      @machinesWrapper.addSubView view, null, shouldPrepend = yes

   addStacksNotConfiguredWarning: ->