Revamping map view to work with AJAX data api

--- frontend/coffeescript/app/viz/map_view.coffee
+++ frontend/coffeescript/app/viz/map_view.coffee
@@ -1,247 +1,79 @@
 define( [ 'jquery',
           'underscore',
           'backbone',
+          'marionette',
           'leaflet',
-          'leaflet_heatmap',
-          'leaflet_cluster' ],

-( $, _, Backbone, L ) ->
+          # Models/collections/etc
+          'app/collections/data'

-    class MapView extends Backbone.View
-        name:           "MapView"
+          # Leaflet plugins
+          'leaflet_cluster',
+          'leaflet_heatmap' ],
+
+( $, _, Backbone, Marionette, L, DataCollection ) ->
+
+    mapIcon = L.icon(
+                iconUrl: '//keep-static.s3.amazonaws.com/img/leaflet/marker-icon.png'
+                iconRetinaUrl: '//keep-static.s3.amazonaws.com/img/leaflet/marker-icon@2x.png'
+                iconSize: [25, 41]
+                iconAnchor: [12, 41]
+                popupAnchor: [1, -34]
+                shadowUrl: '//keep-static.s3.amazonaws.com/img/leaflet/marker-shadow.png'
+                shadowSize: [41, 41]
+                shadowAnchor: [15, 41] )
+
+    class MarkerItemView extends Backbone.Marionette.ItemView
+        initialize: (options) ->
+            @fields = options.fields
+
+        template: ( model ) =>
+            data = { fields: @fields, model: model }
+
+            templ = _.template( '''
+                <% _.each( fields, function( field ) { %>
+
<%= field.name %>: <%= model.data[ field.name ] %>

+                <% }); %> 	''')
+
+            return templ( data )

-        el:             $( '#map_viz' )
-        btn:            $( '#map_btn' )
+    class DataMapView extends Backbone.Marionette.CollectionView
+        el: '#map-viz'
+        itemView: MarkerItemView

-        # List of (possible) columns that represents a location.
-        map_headers:     []
-        # Column(s) selected to use as geopoints.
-        selected_header: undefined
+        _detect_headers: ( fields ) ->
+            @map_headers = []

-        map:            undefined
-        controls:       undefined
-        playback:       undefined
-
-        start_date:     $( '#start_date' )
-        end_date:       $( '#end_date' )
-
-        # Time step variables
-        step_current: 0
-        num_steps: 0
-        quantum: 0
-
-        is_paused: true
-
-        progress: 0
-
-        DataView::pL = []
-        DataView::pLStyle =
-            color: "blue"
-            weight: 0.5
-            opacity: 1
-            smoothFactor: 0.5
-
-        events:
-            "click #pause":             "pause_playback"
-            "click #reset":             "reset_playback"
-
-            "click #time_c":            "time_c"
-            "click #clear":             "clear_lines"
-
-        initialize: ( options ) ->
-            @parent = options.parent
-            @data   = options.data
-            @form   = options.form
-
-            @_detect_headers( @form.attributes.children )
-
-            @mapIcon = L.icon(
-                        iconUrl: '//keep-static.s3.amazonaws.com/img/leaflet/marker-icon.png'
-                        iconRetinaUrl: '//keep-static.s3.amazonaws.com/img/leaflet/marker-icon@2x.png'
-                        iconSize: [25, 41]
-                        iconAnchor: [12, 41]
-                        popupAnchor: [1, -34]
-                        shadowUrl: '//keep-static.s3.amazonaws.com/img/leaflet/marker-shadow.png'
-                        shadowSize: [41, 41]
-                        shadowAnchor: [15, 41] )
-
-            if @map_headers.length > 0
-                @btn.removeClass( 'disabled' )
-                @render()
-
-                # min and max time in milliseconds for the array
-                if @start_date.val().length > 0
-                    @min_time = Date.parse( @start_date.val() )
-                else
-                    @min_time = Date.parse( @data.models[0].get('timestamp') )
-
-                    now = new Date( @min_time )
-                    day      = ( '0' + now.getDate() ).slice( -2 )
-                    month    = ( '0' + (now.getMonth() + 1) ).slice( -2 )
-
-                    @start_date.val( now.getFullYear() + '-' + month + '-' + day )
-
-                if @end_date.val().length > 0
-                    @max_time = Date.parse( @end_date.val() )
-                else
-                    @max_time = Date.parse( @data.models[ @data.models.length - 1 ].get('timestamp') )
-
-                    now = new Date( @max_time )
-                    day      = ( '0' + now.getDate() ).slice( -2 )
-                    month    = ( '0' + (now.getMonth() + 1) ).slice( -2 )
-
-                    @end_date.val( now.getFullYear() + '-' + month + '-' + day )
-
-                # split the time into frames based on fps * playtime
-                @num_steps = $( '#fps' ).val() * $( '#playtime' ).val()
-
-                # calc size of quantum
-                @quantum = Math.floor( (@max_time - @min_time) / @num_steps )
-
-                # set upper and lower bound
-                @lower_bound = @min_time
-                @upper_bound = @min_time + @quantum
-
-        _detect_headers: ( root ) ->
-
-            for field in root
-                if field.type in [ 'group' ]
-                    @_detect_headers( field.children )
-
-                # Detect geopoints
+            for field in fields
                 if field.type in [ 'geopoint' ]
                     @map_headers.push( field )

-                    if not @selected_header?
-                        @selected_header = { location: field }
+        _resize_map: () =>
+            $( '#map' ).css( { 'height': @$el.parent().height() + 'px' } )
+            @map.invalidateSize( false ) if @map?

-                    continue
-                else if field.label.search( 'lat' ) != -1
-                    @map_headers.push( field )
-                else if field.label.search( 'lng' ) != -1
-                    @map_headers.push( field )
+        initialize: ( options ) ->
+            # Set the height of the map to match the full height of the browser
+            @_resize_map()
+            $( window ).resize( @_resize_map )

-            # This means there were no geopoints... attempt to find an lat/lng
-            # combo between two columns
-            if not @selected_header?
+            # Setup and initialize our data collection model.
+            @fields = options.fields
+            @collection = new DataCollection( options )
+            @_detect_headers( @fields )

-                @selected_header = {}
-                for field in @map_headers
-                    if field.label.search( 'lat' ) != -1
-                        @selected_header.lat = field
-                    else if field.label.search( 'lng' ) != -1
-                        @selected_header.lng = field
+            # Setup the map itself
+            @map = L.map( 'map' ).setView( [ 0, 0 ], 10 )
+            L.tileLayer( 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
+                         attribution: '© OpenStreetMap contributors',
+                         maxZoom: 18 }).addTo( @map )

-        _geopoint: ( datum ) ->
-
-            geopoint = undefined
-
-            if @selected_header.location?
-
-                geopoint = datum.get( 'data' )[ @selected_header.location.name ]
-
-                if not geopoint?
-                    return null
-
-                geopoint = geopoint.split( ' ' )[0..2]
-
-                if isNaN( geopoint[0] ) or isNaN( geopoint[1] )
-                    return null
-
-            else
-                geopoint = [ datum.get( 'data' )[ @selected_header.lat.name ],
-                             datum.get( 'data' )[ @selected_header.lng.name ] ]
-
-            geopoint[0] = parseFloat( geopoint[0] )
-            geopoint[1] = parseFloat( geopoint[1] )
-
-            if isNaN( geopoint[0] ) or isNaN( geopoint[1] )
-                return null
-
-            return geopoint
-
-        render: () ->
-            # Calculate the center of the data
-            center = [ 0, 0 ]
-
-            valid_count = 0
-            for datum in @data.models
-
-                geopoint = @_geopoint( datum )
-
-                if not geopoint?
-                    continue
-
-                center[0] += geopoint[0]
-                center[1] += geopoint[1]
-                valid_count += 1
-
-            if valid_count == 0
-                @map_enabled = false
-                $( '#map_btn' ).addClass( 'disabled' )
-                $( '#map' ).hide()
-                return @
-
-            # Create Leaflet map and setup markers, connectiosn and cluster layer
-            center[0] = center[0] / valid_count
-            center[1] = center[1] / valid_count
-
-            @map = L.map( 'map' ).setView( center, 10 )
-            L.tileLayer( 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
-                            attribution: '© OpenStreetMap contributors',
-                            maxZoom: 18 }).addTo( @map )
-
-            # Setup heatmap
+            # Create and initialize the different layers we'll have.
             @heatmap = L.TileLayer.heatMap( { radius: 24, opacity: 0.8 } )
-            heatmap_value = 1.0 / @data.models.length
-
-            heatmapData = []
-            @markers        = new L.layerGroup()
-            @clusters       = new L.MarkerClusterGroup()
-            @connections    = new L.layerGroup()
-
-            last_marker = undefined
-            for datum in @data.models
-                geopoint = @_geopoint( datum )
-
-                if not geopoint?
-                    continue
-
-                marker = L.marker( [geopoint[0], geopoint[1]], {icon: @mapIcon})
-                marker.data = datum.get( 'data' )
-                marker.timestamp = new Date( datum.get( 'timestamp' ) )
-
-                if last_marker?
-                    day = 1000 * 60 * 60 * 24
-                    if marker.timestamp.getTime() - last_marker.timestamp.getTime() < day
-                        pline = [ marker.getLatLng(), last_marker.getLatLng() ]
-                        @connections.addLayer( L.polyline( pline ) )
-
-                    last_marker = marker
-                else
-                    last_marker = marker
-
-                html = ''
-                for key, value of marker.data
-                    html += "
#{key}: #{value}
"
-                marker.bindPopup( html )
-
-                @markers.addLayer( marker )
-                @clusters.addLayer( marker )
-
-                if datum.get( 'data' ).value?
-                    heatmap_value = datum.get( 'data' ).value
-
-                heatmapData.push(
-                    lat: geopoint[0]
-                    lon: geopoint[1]
-                    value: heatmap_value )
-
-            # By default we'll have the heatmap layer enabled and everything
-            # else disabled
-            @heatmap.addData( heatmapData )
-            @map.addLayer( @heatmap )
+            @markers = new L.layerGroup()
+            @clusters = new L.MarkerClusterGroup()
+            @connections = new L.layerGroup()

             layers =
                 'Clusters': @clusters
@@ -252,150 +84,43 @@
             @controls = L.control.layers( null, layers, { collapsed: false } )
             @controls.addTo( @map )

+            # Listen to events
+            @map.on( 'moveend', @refresh_viewport )
+
+            # Clear all our markers when we get another set of markers.
+            @collection.on( 'reset', ()=>
+                @markers.clearLayers()
+                @clusters.clearLayers() )
             @

-        pause_playback: (event) ->
-            if @is_paused
-                $( '#pause' ).html( "" )
-                @is_paused = false
+        appendHtml: (collectionView, itemView, index) ->
+            # Instead of appending these views into the DOM, place them in the
+            # map instead!
+            point = itemView.model.geopoint( collectionView.map_headers[0] )
+            marker = L.marker( point, {icon: mapIcon} )
+            marker.bindPopup( itemView.el )

-                # Start up playback interval if it's not already started up.
-                if not @playback?
-                    auto = () =>
-                        @time_step() if not @is_paused
+            # Add marker to our different layers
+            collectionView.markers.addLayer( marker )
+            collectionView.clusters.addLayer( marker )

-                    @playback = setInterval( auto, 1000.0 / $( '#fps' ).val() )
+        buildItemView: ( item, ItemViewType, itemViewOptions ) ->
+            options = _.extend( { model: item, fields: @fields }, itemViewOptions )
+            return new ItemViewType( options )

-            else
-                $( '#pause' ).html( "" )
-                @is_paused = true
+        onShow: () ->
+            @map.invalidateSize( false )

-        # resets playback
-        reset_playback: (event) ->
-            if @playback?
-                clearInterval( @playback )
-                @playback = null
-
-            # set initial lower and upper bound of our current quantum
-            @lower_bound = @min_time
-            @upper_bound = @min_time + @quantum
-
-            @step_current = 0
-
-            $( '#current_time' ).html( '' )
-
-            $( '#progress_bar > .bar' ).width( 0 )
-
-            $( '#pause_btn' ).html( '<icon class="icon-play">' )
-            @is_paused = false
-
-        clear_lines: (event) ->
-            for i of @map._layers
-                unless @map._layers[i]._path is `undefined`
-                    try
-                        @map.removeLayer @map._layers[i]
-                    catch e
-                        console.log "problem with " + e + @map._layers[i]
-
-        time_step: (event) ->
-
-            @step_current += 1
-
-            # If we've made it past the number steps, pause the playback and
-            # return.
-            if @step_current > @num_steps
-                @is_paused = true
-                return @
-
-            # Go through each marker in the layer and enable/disable the
-            # marker if it meets our timestamp constraints.
-            last_marker = undefined
-            @markers.eachLayer( ( layer ) =>
-
-                if @lower_bound <= layer.timestamp <= @upper_bound
-                    layer.setOpacity( 1.0 )
-                    last_marker = layer
-                else
-                    layer.setOpacity( 0.0 )
-            )
-
-            # Pan to the last marker that was enabled
-            @map.panTo( last_marker.getLatLng() ) if last_marker?
-
-            # Calculate the percent progress we have progressed.
-            @progress = 100 * ( @upper_bound - @min_time ) / ( @max_time - @min_time )
-
-            # display the range of the current quantum
-            lower_bound_str = new Date( @lower_bound )
-            upper_bound_str = new Date( @upper_bound )
-
-            $( '#current_time' ).html( "#{lower_bound_str} through #{upper_bound_str}" )
-            $( '#progress_bar > .bar' ).width( "#{@progress}%" )
-
-            @upper_bound += @quantum
+        refresh_viewport: ( event ) =>
+            # Everytime we move around, grab new data from the server and
+            # refresh the viewport!
+            bounds = event.target.getBounds()
+            @collection.fetch(
+                reset: true
+                data:
+                    bbox: bounds.toBBoxString() )

             @

-        time_c: (event) ->
-            val = $("#tc_input").val()
-            con = $("input:radio[name=const]:checked").val()
-            time = $("input:radio[name=time]:checked").val()
-            minutes = 1000 * 60
-            hours = minutes * 60
-            days = hours * 24
-            secs = undefined
-            for i of @map._layers
-                unless @map._layers[i]._path is `undefined`
-                    try
-                        @map.removeLayer @map._layers[i]
-                    catch e
-                        console.log "problem with " + e + @map._layers[i]
-            if con is "day"
-                secs = days
-            else if con is "hour"
-                secs = hours
-            else secs = minutes  if con is "minute"
-            _ref5 = @data.models
-            _j = 0
-            _len1 = _ref5.length
-
-            while _j < (_len1 - 1)
-                datum = _ref5[_j]
-                parseDate = d3.time.format("%Y-%m-%dT%H:%M:%S").parse
-                if time is "date"
-                    try
-                        start = Date.parse(@data.models[_j].attributes.data.Time)
-                    catch err
-                        start = null
-                else
-                    start = parseDate(@data.models[_j].get("timestamp"))
-                d = Math.floor(start / secs)
-                geopoint = datum.get("data")[@map_headers].split(" ")
-                @pL = []
-                @pL.push [parseFloat(geopoint[0]), parseFloat(geopoint[1])]
-                _k = _j + 1
-                _len1 = _ref5.length
-
-                while _k < _len1
-                    break  if start = null
-                    datum2 = _ref5[_k]
-                    parseDate = d3.time.format("%Y-%m-%dT%H:%M:%S").parse
-                    if time is "date"
-                        try
-                            start2 = Date.parse(@data.models[_k].attributes.data.Time)
-                        catch err
-                            start2 = null
-                    else
-                        start2 = parseDate(@data.models[_k].get("timestamp"))
-                    d2 = Math.floor(start2 / secs)
-                    geopoint = datum2.get("data")[@map_headers].split(" ")
-                    if val >= Math.abs(d - d2)
-                        @pL.push [parseFloat(geopoint[0]), parseFloat(geopoint[1])]
-                        @poly = L.polyline(@pL, @pLStyle).addTo(@map)
-                    else
-                        break
-                    _k++
-                _j++
-
-    return MapView
+    return DataMapView
 )


Try/catch blocks for catching connection errors

--- src/scripts/harvest.coffee
+++ src/scripts/harvest.coffee
@@ -100,12 +100,15 @@

     # If the credentials are valid, remember them, otherwise
     # tell the user they are wrong.
-    account.test msg, (valid) ->
-      if valid
-        msg.message.user.harvest_account = account
-        msg.reply "Thanks, Iʼll remember your credentials. Have fun with Harvest."
-      else
-        msg.reply "Uh-oh – I just tested your credentials, but they appear to be wrong. Please specify the correct ones."
+    try
+      account.test msg, (valid) ->
+        if valid
+          msg.message.user.harvest_account = account
+          msg.reply "Thanks, Iʼll remember your credentials. Have fun with Harvest."
+        else
+          msg.reply "Uh-oh – I just tested your credentials, but they appear to be wrong. Please specify the correct ones."
+    catch error
+      msg.reply "Fatal error: #{error}"

   # Allows a user to delete his credentials.
   robot.respond /forget my harvest account/i, (msg) ->
@@ -117,31 +120,37 @@
     unless user = check_user(robot, msg, msg.match[2])
       return

-    user.harvest_account.daily msg, (status, body) ->
-      if 200 <= status <= 299
-        msg.reply "Your entries for today, #{user.name}:"
-        for entry in body.day_entries
-          if entry.ended_at == ""
-            msg.reply "• #{entry.project} (#{entry.client}) → #{entry.task} <#{entry.notes}> [running since #{entry.started_at} (#{entry.hours}h)]"
-          else
-            msg.reply "• #{entry.project} (#{entry.client}) → #{entry.task} <#{entry.notes}> [#{entry.started_at} – #{entry.ended_at} (#{entry.hours}h)]"
-      else
-        msg.reply "Request failed with status #{status}."
+    try
+      user.harvest_account.daily msg, (status, body) ->
+        if 200 <= status <= 299
+          msg.reply "Your entries for today, #{user.name}:"
+          for entry in body.day_entries
+            if entry.ended_at == ""
+              msg.reply "• #{entry.project} (#{entry.client}) → #{entry.task} <#{entry.notes}> [running since #{entry.started_at} (#{entry.hours}h)]"
+            else
+              msg.reply "• #{entry.project} (#{entry.client}) → #{entry.task} <#{entry.notes}> [#{entry.started_at} – #{entry.ended_at} (#{entry.hours}h)]"
+        else
+          msg.reply "Request failed with status #{status}."
+    catch error
+      msg.reply("Fatal error: #{error}")

   # List all project/task combinations that are available to a user.
   robot.respond /list harvest tasks( of (.+))?/i, (msg) ->
     unless user = check_user(robot, msg, msg.match[2])
       return

-    user.harvest_account.daily msg, (status, body) ->
-      if 200 <= status <= 299
-        msg.reply "The following project/task combinations are available for you, #{user.name}:"
-        for project in body.projects
-          msg.reply "• Project #{project.name}"
-          for task in project.tasks
-            msg.reply "  ‣ #{task.name} (#{if task.billable then 'billable' else 'non-billable'})"
-      else
-        msg.reply "Request failed with status #{status}."
+    try
+      user.harvest_account.daily msg, (status, body) ->
+        if 200 <= status <= 299
+          msg.reply "The following project/task combinations are available for you, #{user.name}:"
+          for project in body.projects
+            msg.reply "• Project #{project.name}"
+            for task in project.tasks
+              msg.reply "  ‣ #{task.name} (#{if task.billable then 'billable' else 'non-billable'})"
+        else
+          msg.reply "Request failed with status #{status}."
+    catch error
+      msg.reply "Fatal error: #{error}"

   # Kick off a new timer, stopping the previously running one, if any.
   robot.respond /start harvest at (.+)\/(.+): (.*)/i, (msg) ->
@@ -151,14 +160,17 @@
     project = msg.match[1]
     task    = msg.match[2]
     notes   = msg.match[3]
-
-    user.harvest_account.start msg, project, task, notes, (status, body) ->
-      if 200 <= status <= 299
-        if body.hours_for_previously_running_timer?
-          msg.reply "Previously running timer stopped at #{body.hours_for_previously_running_timer}h."
-        msg.reply "OK, I started tracking you on #{body.project}/#{body.task}."
-      else
-        msg.reply "Request failed with status #{status}."
+
+    try
+      user.harvest_account.start msg, project, task, notes, (status, body) ->
+        if 200 <= status <= 299
+          if body.hours_for_previously_running_timer?
+            msg.reply "Previously running timer stopped at #{body.hours_for_previously_running_timer}h."
+          msg.reply "OK, I started tracking you on #{body.project}/#{body.task}."
+        else
+          msg.reply "Request failed with status #{status}."
+    catch error
+      msg.reply "Fatal error: #{error}"

   # Stops the timer running for a project/task combination,
   # if any. If no combination is given, stops the first
@@ -170,18 +182,24 @@
     if msg.match[1]
       project = msg.match[2]
       task    = msg.match[3]
-      user.harvest_account.stop msg, project, task, (status, body) ->
-        if 200 <= status <= 299
-          msg.reply "Timer stopped (#{body.hours}h)."
-        else
-          msg.reply "Request failed with status #{status}."
-          msg.reply body
+      try
+        user.harvest_account.stop msg, project, task, (status, body) ->
+          if 200 <= status <= 299
+            msg.reply "Timer stopped (#{body.hours}h)."
+          else
+            msg.reply "Request failed with status #{status}."
+            msg.reply body
+      catch error
+        msg.reply("Fatal error: #{error}")
     else
-      user.harvest_account.stop_first msg, (status, body) ->
-        if 200 <= status <= 299
-          msg.reply "Timer stopped (#{body.hours}h)."
-        else
-          msg.reply "Request failed with status #{status}."
+      try
+        user.harvest_account.stop_first msg, (status, body) ->
+          if 200 <= status <= 299
+            msg.reply "Timer stopped (#{body.hours}h)."
+          else
+            msg.reply "Request failed with status #{status}."
+      catch error
+        msg.reply("Fatal error: #{error}")

 # Class managing the Harvest account associated with
 # a user. Keeps track of the user's credentials and can


Fix Manatee script: Heed location in redirect

--- src/scripts/manatee.coffee
+++ src/scripts/manatee.coffee
@@ -2,8 +2,7 @@
 #   Allows Hubot to pull down images from calmingmanatee.com
 #
 # Dependencies:
-#   "htmlparser": "1.7.6"
-#   "soupselect": "0.2.0"
+#   None
 #
 # Configuration:
 #   None
@@ -14,26 +13,18 @@
 # Author:
 #   Danny Lockard

-Select = require( "soupselect" ).select
-HTMLParser = require "htmlparser"

 module.exports = (robot) ->
         robot.respond /manatee/i, (msg) ->
-                options = {
-                        "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
-                }
                 msg
                         .http( 'http://calmingmanatee.com' )
-                        .header('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6')
-                        .get(options) (err, res, body) ->
+                        .headers('User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6')
+                        .get() (err, res, body) ->
                                 if err
                                         msg.send "Something went wrong #{err}"
                                         return
-                                msg.send "http://calmingmanatee.com/" + get_manatee(body, "body div#holder img")

-get_manatee = (body, selector)->
-        html_handler = new HTMLParser.DefaultHandler((()->), ignoreWhitespace: true)
-        html_parser = new HTMLParser.Parser html_handler
-
-        html_parser.parseComplete body
-        Select(html_handler.dom, selector)[0].attribs.src
+                                url = res.headers.location
+                                manatee_no = url.substring(url.lastIndexOf("/") + 1)
+
+                                msg.send "http://calmingmanatee.com/img/manatee#{manatee_no}.jpg"


Add pronunciation to Wordnik

Add pronunciation to Wordnik

--- src/scripts/wordnik.coffee
+++ src/scripts/wordnik.coffee
@@ -1,34 +1,52 @@
 # Dictionary definitions with the Wordnik API. You'll need an API key from http://developer.wordnik.com/
 #
 # define me <word> - Grabs a dictionary definition of a word.
+# pronounce me <word> - Links to a pronunciation of a word.
+
 module.exports = (robot) ->
+  # Definition
   robot.respond /define( me)? (.*)/i, (msg) ->
-
-    if process.env.WORDNIK_API_KEY == undefined
-      msg.send "Missing WORDNIK_API_KEY env variable."
-      return
-
     word = msg.match[2]

-    msg.http("http://api.wordnik.com/v4/word.json/#{escape(word)}/definitions")
-      .header('api_key', process.env.WORDNIK_API_KEY)
-      .get() (err, res, body) ->
-        definitions = JSON.parse(body)
+    fetch_wordnik_resource(msg, word, 'definitions') (err, res, body) ->
+      definitions = JSON.parse(body)

-        if definitions.length == 0
-          msg.send "No definitions for \"#{word}\" found."
+      if definitions.length == 0
+        msg.send "No definitions for \"#{word}\" found."
+      else
+        reply = "Definitions for \"#{word}\":\n"
+        lastSpeechType = null
+
+        definitions = definitions.forEach (def) ->
+          # Show the part of speech (noun, verb, etc.) when it changes
+          if def.partOfSpeech != lastSpeechType
+            reply += " (#{def.partOfSpeech})\n" if def.partOfSpeech != undefined
+
+          # Track the part of speech
+          lastSpeechType = def.partOfSpeech
+
+          # Add the definition
+          reply += "  - #{def.text}\n"
+
+        msg.send reply
+
+  robot.respond /(pronounce|enunciate)( me)? (.*)/i, (msg) ->
+    word = msg.match[3]
+
+    fetch_wordnik_resource(msg, word, 'audio') (err, res, body) ->
+        pronunciations = JSON.parse(body)
+
+        if pronunciations.length == 0
+          msg.send "No pronounciation for \"#{word}\" found."
         else
-          reply = "Definitions for \"#{word}\":\n"
-
-          lastSpeechType = null
-          definitions = definitions.forEach (def) ->
-            # Show the part of speech (noun, verb, etc.) when it changes
-            if def.partOfSpeech != lastSpeechType
-              reply += " (#{def.partOfSpeech})\n" if def.partOfSpeech != undefined
-
-            lastSpeechType = def.partOfSpeech
-
-            reply += "  - #{def.text}\n"
-
-          msg.send reply
+          pronunciation = pronunciations[0]
+          msg.send pronunciation.fileUrl

+fetch_wordnik_resource = (msg, word, resource, callback) ->
+  if process.env.WORDNIK_API_KEY == undefined
+    msg.send "Missing WORDNIK_API_KEY env variable."
+    return
+
+  msg.http("http://api.wordnik.com/v4/word.json/#{escape(word)}/#{escape(resource)}")
+    .header('api_key', process.env.WORDNIK_API_KEY)
+    .get(callback)


webapp/misc: correctly specifying list of values for defaultOpen

--- src/smc-webapp/r_misc.cjsx
+++ src/smc-webapp/r_misc.cjsx
@@ -1136,22 +1136,22 @@
         on_blur     : rtypes.func
         autoFocus   : rtypes.bool
         onKeyDown   : rtypes.func
-        defaultOpen : rtypes.string
+        defaultOpen : rtypes.oneOf([false, 'time', 'date'])

     getDefaultProps: ->
         defaultOpen : 'date'

     render: ->
         <DateTimePicker
-            step       = {60}
-            editFormat = {'MMM d, yyyy h:mm tt'}
-            format     = {'MMM d, yyyy h:mm tt'}
-            parse      = {DATETIME_PARSE_FORMATS}
-            value      = {@props.value}
-            onChange   = {@props.on_change}
-            onFocus    = {@props.on_focus}
-            onBlur     = {@props.on_blur}
-            autoFocus  = {@props.autoFocus}
+            step        = {60}
+            editFormat  = {'MMM d, yyyy h:mm tt'}
+            format      = {'MMM d, yyyy h:mm tt'}
+            parse       = {DATETIME_PARSE_FORMATS}
+            value       = {@props.value}
+            onChange    = {@props.on_change}
+            onFocus     = {@props.on_focus}
+            onBlur      = {@props.on_blur}
+            autoFocus   = {@props.autoFocus}
             defaultOpen = {@props.defaultOpen}
         />


If no nearby results, change to remote ones when modal opens, #1181 (#1191)


Current page reload when ineject API key


[deploy : production]

[deploy : production]


shopify callback added

shopify callback added


add comment

add comment

--- services/web/app/coffee/Features/User/UserGetter.coffee
+++ services/web/app/coffee/Features/User/UserGetter.coffee
@@ -37,6 +37,7 @@
 		if arguments.length == 2
 			callback = projection
 			projection = {}
+		# $exists: true MUST be set to use the partial index
 		query = emails: { $exists: true }, 'emails.email': email
 		db.users.findOne query, projection, (error, user) =>
 			return callback(error, user) if error? or user?


Update interval tag

Update interval tag

--- test/spec/sync/iteration/interval.tests.coffee
+++ test/spec/sync/iteration/interval.tests.coffee
@@ -16,7 +16,7 @@
   DATE_START = new Date('2013-06-09T08:00:00.000Z')
   DATE_STEP_MS = 1000

-  describe "Model.interval #{options.$parameter_tags or ''}#{options.$tags} @slow", ->
+  describe "Model.interval #{options.$parameter_tags or ''}#{options.$tags} @interval @slow", ->
     Flat = null
     before ->
       BackboneORM.configure {model_cache: {enabled: !!options.cache, max: 100}}


add V1HistoryNotSyncedError

add V1HistoryNotSyncedError

--- services/web/app/coffee/Features/Errors/Errors.coffee
+++ services/web/app/coffee/Features/Errors/Errors.coffee
@@ -47,6 +47,13 @@
 	return error
 UnsupportedExportRecordsError.prototype.__proto___ = Error.prototype

+V1HistoryNotSyncedError = (message) ->
+	error = new Error(message)
+	error.name = "V1HistoryNotSyncedError"
+	error.__proto__ = V1HistoryNotSyncedError.prototype
+	return error
+V1HistoryNotSyncedError.prototype.__proto___ = Error.prototype
+
 ProjectHistoryDisabledError = (message) ->
 	error = new Error(message)
 	error.name = "ProjectHistoryDisabledError "
@@ -62,4 +69,5 @@
 	UnsupportedFileTypeError: UnsupportedFileTypeError
 	UnsupportedBrandError: UnsupportedBrandError
 	UnsupportedExportRecordsError: UnsupportedExportRecordsError
+	V1HistoryNotSyncedError: V1HistoryNotSyncedError
 	ProjectHistoryDisabledError: ProjectHistoryDisabledError


finally fixed keyboard focusing on mobile devices

--- src/art/engine/core/canvas_element.coffee
+++ src/art/engine/core/canvas_element.coffee
@@ -495,8 +495,9 @@
       @keyUpEvent keyboardEvent

   _enableHtmlFocusOnCanvas: ->
-    @_canvas.tabIndex = "-1" unless isMobileBrowser()
-    @_canvas.contentEditable = true
+    unless isMobileBrowser()
+      @_canvas.tabIndex = "-1"
+      @_canvas.contentEditable = true

   _attachDomEventListeners: ->
     return if @_eventListenersAttached


Small code cleanup - remove unused vars, commented code

--- prototypes/using-sirius/coffeescripts/extensions/Event.coffee
+++ prototypes/using-sirius/coffeescripts/extensions/Event.coffee
@@ -25,7 +25,6 @@

 window.sirius.Event::resolve_references = (deferred, object_with_id) ->
   deferred.push =>
-    obj = node_id: 0, link_id: 0, network_id: 0
     console.log @attributes
     scenario_elements = @get('targetelements').get('scenarioelement')
     console.log scenario_elements
@@ -51,9 +50,6 @@
     @set 'link', link
     @set 'network', network

-    #if !node_id and !link_id and !network_id
-    #  throw "Event must have node_id, link_id, or network_id"
-
 window.sirius.Event::encode_references = ->
   @set('node_id', @get('node').id) if @has('node')
   @set('link_id', @get('link').id) if @has('link')


using D.socket without assigning it to a variable

--- client/ide.coffee
+++ client/ide.coffee
@@ -3,7 +3,6 @@
   overlap = (x0, x1, x2, x3) -> x2 < x1 && x0 < x3 # helper: do the segments (x0,x1) and (x2,x3) overlap?

   D.idePage = ->
-    {socket} = D
     $('body').html """

@@ -63,9 +62,9 @@

     D.wins = wins = # mapping between window ids and widget instances (D.Session or D.Editor)
       0: session = D.Session $('.ui-layout-center'),
-        edit: (s, i) -> socket.emit 'Edit', win: 0, pos: i, text: s
-        autocomplete: (s, i) -> socket.emit 'Autocomplete', line: s, pos: i, token: 0
-        exec: (lines, trace) -> (if !trace then execQueue = lines[1..]); socket.emit 'Execute', {trace, text: lines[0] + '\n'}; return
+        edit: (s, i) -> D.socket.emit 'Edit', win: 0, pos: i, text: s
+        autocomplete: (s, i) -> D.socket.emit 'Autocomplete', line: s, pos: i, token: 0
+        exec: (lines, trace) -> (if !trace then execQueue = lines[1..]); D.socket.emit 'Execute', {trace, text: lines[0] + '\n'}; return

     popWindow = (w) ->
       if !opener
@@ -77,14 +76,14 @@
           $.alert 'Popups are blocked.'
       return

-    socket
+    D.socket
       .on '*identify', (i) -> D.remoteIdentification = i; return
       .on 'UpdateDisplayName', ({displayName}) -> $('title').text displayName
       .on 'EchoInput', ({input}) -> session.add input
       .on 'AppendSessionOutput', ({result}) -> session.add result
       .on 'NotAtInputPrompt', -> session.noPrompt()
       .on 'AtInputPrompt', ({why}) ->
-        if execQueue.length then socket.emit 'Execute', trace: 0, text: execQueue.shift() + '\n' else session.prompt why
+        if execQueue.length then D.socket.emit 'Execute', trace: 0, text: execQueue.shift() + '\n' else session.prompt why
         return
       .on 'HadError', -> execQueue.splice 0, execQueue.length; return
       .on 'FocusWindow', ({win}) -> $("#wintab#{win} a").click(); wins[win].focus(); return
@@ -103,19 +102,19 @@
         $tabContent = $("

").appendTo('.ui-layout-' + dir)
         wins[w] = D.Editor $tabContent,
           debugger: ee.debugger
-          save: (s, bs)   -> socket.emit 'SaveChanges',    win: w, text: s, attributes: stop: bs
-          close:          -> socket.emit 'CloseWindow',    win: w
-          over:           -> socket.emit 'RunCurrentLine', win: w
-          into:           -> socket.emit 'StepInto',       win: w
-          back:           -> socket.emit 'TraceBackward',  win: w
-          skip:           -> socket.emit 'TraceForward',   win: w
-          continueTrace:  -> socket.emit 'ContinueTrace',  win: w
-          continueExec:   -> socket.emit 'Continue',       win: w
-          restartThreads: -> socket.emit 'RestartThreads', win: w
-          edit:    (s, p) -> socket.emit 'Edit',           win: w, text: s, pos: p
-          interrupt:      -> socket.emit 'WeakInterrupt'
-          cutback:        -> socket.emit 'Cutback',        win: w
-          autocomplete: (s, i) -> socket.emit 'autocomplete', s, i, w
+          save: (s, bs)   -> D.socket.emit 'SaveChanges',    win: w, text: s, attributes: stop: bs
+          close:          -> D.socket.emit 'CloseWindow',    win: w
+          over:           -> D.socket.emit 'RunCurrentLine', win: w
+          into:           -> D.socket.emit 'StepInto',       win: w
+          back:           -> D.socket.emit 'TraceBackward',  win: w
+          skip:           -> D.socket.emit 'TraceForward',   win: w
+          continueTrace:  -> D.socket.emit 'ContinueTrace',  win: w
+          continueExec:   -> D.socket.emit 'Continue',       win: w
+          restartThreads: -> D.socket.emit 'RestartThreads', win: w
+          edit:    (s, p) -> D.socket.emit 'Edit',           win: w, text: s, pos: p
+          interrupt:      -> D.socket.emit 'WeakInterrupt'
+          cutback:        -> D.socket.emit 'Cutback',        win: w
+          autocomplete: (s, i) -> D.socket.emit 'autocomplete', s, i, w
           pop: -> popWindow w
           openInExternalEditor: D.openInExternalEditor
         wins[w].open ee


Overlay should not include toolbar - toolbar should have a higher z-index so that it's still available.

--- plugins/overlay.coffee
+++ plugins/overlay.coffee
@@ -142,9 +142,9 @@

         _getMeasures: ->
             m =
-                editableHeight: @options.toolbar.outerHeight() + @options.currentEditable.outerHeight()
+                editableHeight: @options.currentEditable.outerHeight()
                 editableWidth: @options.currentEditable.outerWidth()
-                editableTop: parseInt @options.toolbar.css 'top'
+                editableTop: parseInt @options.currentEditable.offset().top
                 editableLeft: parseInt @options.currentEditable.offset().left
                 windowWidth: jQuery(window).width()
                 documentHeight: jQuery(window.document).height()
@@ -159,5 +159,4 @@
                 backgroundColor: @options.backgroundColor
                 opacity: @.options.opacity

-
 )(jQuery)


Autocompletion is not working on modal.

Autocompletion is not working on modal.

--- app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
@@ -380,7 +380,7 @@
 #            @log 'selected', event, ui
             b(event, ui.item.id)
         )
-      @delay( a, 80 )
+      @delay( a, 180 )

     # input
     else


Improved form detection.

Improved form detection.

--- app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller_form.js.coffee
@@ -29,6 +29,8 @@
     App.Log.debug 'ControllerForm', 'formGen', @model.configure_attributes

     fieldset = $('<fieldset></fieldset>')
+
+    fieldset.append('' + App.i18n.translatePlain('Discard your unsaved changes.') + '')

     # collect form attributes
     @attributes = []
@@ -1595,38 +1597,16 @@
   @params: (form) ->
     param = {}

-    # create jquery object if not already exists
-    if form instanceof jQuery
-      # do nothing
-    else
-      form = $(form)
-
-    # find form if current is <form>
-    if form.is('form')
-      form = form
-
-    # find form based on sub elements
-    else if form.find('form')[0]
-      form = $( form.find('form')[0] )
-
-    # find form based on parents next <form>
-    else if form.parents('form')[0]
-      form = $( form.parents('form')[0] )
-
-    # use current content as form if form isn't already finished
-    else if !@finishForm
-      from = form
-    else
-      App.Log.error 'ControllerForm', 'no form found!', form
+    lookupForm = @findForm(form)

     # get form elements
-    array = form.serializeArray()
+    array = lookupForm.serializeArray()

     # 1:1 and boolean params
     for key in array

       # check if item is-hidden and should not be used
-      if form.find('[name="' + key.name + '"]').hasClass('is-hidden')
+      if lookupForm.find('[name="' + key.name + '"]').hasClass('is-hidden')
         continue

       # collect all other params
@@ -1694,29 +1674,73 @@
     formId = new Date().getTime() + Math.floor( Math.random() * 99999 )
     formId.toString().substr formId.toString().length-9, 9

+  @findForm: (form) ->
+    # check jquery event
+    if form && form.target
+      form = form.target
+
+    # create jquery object if not already exists
+    if form instanceof jQuery
+      # do nothing
+    else
+      form = $(form)
+
+    #console.log('FF', form)
+    # get form
+    if form.is('form') is true
+      #console.log('direct from')
+      return form
+    else if form.find('form').is('form') is true
+      #console.log('child from')
+      return form.find('form')
+    else if $(form).parents('form').is('form') is true
+      #console.log('parent from')
+      return form.parents('form')
+    # use current content as form if form isn't already finished
+    else if !@finishForm
+      #console.log('finishForm')
+      return form
+    else
+      App.Log.error 'ControllerForm', 'no form found!', form
+    form
+
   @disable: (form) ->
-    App.Log.notice 'ControllerForm', 'disable...', $(form.target).parent()
-    $(form.target).parent().find('button').attr('disabled', true)
-    $(form.target).parent().find('[type="submit"]').attr('disabled', true)
-    $(form.target).parent().find('[type="reset"]').attr('disabled', true)
-
+    lookupForm = @findForm(form)
+
+    if lookupForm
+      App.Log.notice 'ControllerForm', 'disable...', lookupForm
+      lookupForm.find('button').attr('disabled', true)
+      lookupForm.find('[type="submit"]').attr('disabled', true)
+      lookupForm.find('[type="reset"]').attr('disabled', true)
+    else
+      App.Log.notice 'ControllerForm', 'disable item...', form
+      form.attr('disabled', true)

   @enable: (form) ->
-    App.Log.notice 'ControllerForm', 'enable...', $(form.target).parent()
-    $(form.target).parent().find('button').attr('disabled', false)
-    $(form.target).parent().find('[type="submit"]').attr('disabled', false)
-    $(form.target).parent().find('[type="reset"]').attr('disabled', false)
+
+    lookupForm = @findForm(form)
+
+    if lookupForm
+      App.Log.notice 'ControllerForm', 'enable...', lookupForm
+      lookupForm.find('button').attr('disabled', false)
+      lookupForm.find('[type="submit"]').attr('disabled', false)
+      lookupForm.find('[type="reset"]').attr('disabled', false)
+    else
+      App.Log.notice 'ControllerForm', 'enable item...', form
+      form.attr('disabled', false)

   @validate: (data) ->

+    lookupForm = @findForm(data.form)
+
     # remove all errors
-    $(data.form).parents().find('.has-error').removeClass('has-error')
-    $(data.form).parents().find('.help-inline').html('')
+    lookupForm.find('.has-error').removeClass('has-error')
+    lookupForm.find('.help-inline').html('')

     # show new errors
     for key, msg of data.errors
-      $(data.form).parents('form').find('[name="' + key + '"]').parents('div .form-group').addClass('has-error')
-      $(data.form).parents('form').find('[name="' + key + '"]').parent().find('.help-inline').html(msg)
+      lookupForm.find('[name="' + key + '"]').parents('div .form-group').addClass('has-error')
+      lookupForm.find('[name="' + key + '"]').parent().find('.help-inline').html(msg)

     # set autofocus
-    $(data.form).parents('form').find('.has-error').find('input, textarea').first().focus()
+    lookupForm.find('.has-error').find('input, textarea').first().focus()


Revert events.coffee changes; we definitely need a pointer to the original node because sometimes event.target isn't what we actually want.

--- src/view/dom/events.coffee
+++ src/view/dom/events.coffee
@@ -2,9 +2,15 @@

 # `Batman.DOM.events` contains the Batman.helpers used for binding to events. These aren't called by
 # DOM directives, but are used to handle specific events by the `data-event-#{name}` helper.
-Batman.DOM.events = {
+Batman.DOM.events =
   click: (node, callback, context, eventName = 'click') ->
-    Batman.DOM.addEventListener node, eventName, callbackForEvent.click(callback, context)
+    Batman.DOM.addEventListener node, eventName, (event, args...) ->
+      return if event.metaKey || event.ctrlKey
+
+      Batman.DOM.preventDefault event
+      return if not Batman.DOM.eventIsAllowed(eventName, event)
+
+      callback node, event, args..., context

     if node.nodeName.toUpperCase() is 'A' and not node.href
       node.href = '#'
@@ -30,64 +36,33 @@
       else ['change']

     for eventName in eventNames
-      Batman.DOM.addEventListener node, eventName, callbackForEvent.change(callback, context)
+      Batman.DOM.addEventListener node, eventName, (args...) ->
+        callback node, args..., context

   isEnter: (ev) -> (13 <= ev.keyCode <= 14) || (13 <= ev.which <= 14) || ev.keyIdentifier is 'Enter' || ev.key is 'Enter'

   submit: (node, callback, context) ->
     if Batman.DOM.nodeIsEditable(node)
-      Batman.DOM.addEventListener node, 'keydown', callbackForEvent.keyDown(callback, context)
-      Batman.DOM.addEventListener node, 'keyup',   callbackForEvent.keyUp(callback, context)
+      Batman.DOM.addEventListener node, 'keydown', (args...) ->
+        if Batman.DOM.events.isEnter(args[0])
+          Batman.DOM._keyCapturingNode = node
+      Batman.DOM.addEventListener node, 'keyup', (args...) ->
+        if Batman.DOM.events.isEnter(args[0])
+          if Batman.DOM._keyCapturingNode is node
+            Batman.DOM.preventDefault args[0]
+            callback node, args..., context
+          Batman.DOM._keyCapturingNode = null
     else
-      Batman.DOM.addEventListener node, 'submit',  callbackForEvent.submit(callback, context)
+      Batman.DOM.addEventListener node, 'submit', (args...) ->
+        Batman.DOM.preventDefault args[0]
+        callback node, args..., context
+
     node

-  other: (node, eventName, callback, context) ->
-    Batman.DOM.addEventListener node, eventName, callbackForEvent.other(callback, context)
-}
+  other: (node, eventName, callback, context) -> Batman.DOM.addEventListener node, eventName, (args...) -> callback node, args..., context

 Batman.DOM.eventIsAllowed = (eventName, event) ->
   if delegate = Batman.currentApp?.shouldAllowEvent?[eventName]
     return false if delegate(event) is false

   return true
-
-# we keep these closures separate so we don't maintain references to unwanted variables like node
-
-callbackForEvent = {
-  click: (block, context) ->
-    (event) ->
-      return if event.metaKey || event.ctrlKey
-
-      Batman.DOM.preventDefault event
-      return if not Batman.DOM.eventIsAllowed(event.type, event)
-
-      block(event.target, event, context)
-
-  change: (block, context) ->
-    (event) ->
-      block(event.target, event, context)
-
-  keyDown: (block, context) ->
-    (event) ->
-      if Batman.DOM.events.isEnter(event)
-        Batman.DOM._keyCapturingNode = event.target
-
-  keyUp: (block, context) ->
-    (event) ->
-      if Batman.DOM.events.isEnter(event)
-        node = event.target
-        if Batman.DOM._keyCapturingNode is node
-          Batman.DOM.preventDefault(event)
-          block(node, event, context)
-        Batman.DOM._keyCapturingNode = null
-
-  submit: (block, context) ->
-    (event) ->
-      Batman.DOM.preventDefault(event)
-      block(event.target, event, context)
-
-  other: (block, context) ->
-    (event) ->
-      block(event.target, event, context)
-}


Change order of defined globals: env variable should always replace the GLOBAL, evne if its defined in config as a function

--- gulp/config.coffee
+++ gulp/config.coffee
@@ -243,6 +243,13 @@
     @GLOBALS = require('extend') true, {}, @_GLOBALS_DEFAULTS.defaults, (@_GLOBALS_DEFAULTS[gutil.env.env || "development"] || {})

     Object.keys(@GLOBALS).forEach (k) =>
+      # If a @GLOBALS[k] is a function,
+      # then let's use it as a dynamic getter
+      # (More information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
+      if typeof @GLOBALS[k] == "function"
+        getter = @GLOBALS[k]
+        Object.defineProperty @GLOBALS, k, get: => getter(@GLOBALS)
+
       # You can replace any of @GLOBALS by defining ENV variable in your command line,
       # f.e. `BUNDLE_ID="com.different.bundleid" gulp`
       @GLOBALS[k] = process.env[k] if process.env[k]?
@@ -251,13 +258,5 @@
       # `gulp --BUNDLE_ID="com.different.bundleid"`
       @GLOBALS[k] = gulp.env[k] if gulp.env[k]?

-      # Last but not least, if a @GLOBALS[k] is a function,
-      # then let's define it as a dynamic getter
-      #
-      # More information: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
-      if typeof @GLOBALS[k] == "function"
-        getter = @GLOBALS[k]
-        Object.defineProperty @GLOBALS, k, get: => getter(@GLOBALS)
-
     @filterPublicGlobals(@_PUBLIC_GLOBALS_KEYS)
     @filterShellGlobals(@_SHELL_GLOBALS_KEYS)


毎時のメッセージを修正。

毎時のメッセージを修正。

--- scripts/cron.coffee
+++ scripts/cron.coffee
@@ -8,10 +8,10 @@

 module.exports = (robot) ->
   cronjob = new cronJob(
-    cronTime: '0 0 * * * *'     # 毎時00分00秒
+    cronTime: '0 0 * * * *'     # 毎時00分00秒（秒 分 時 日 月 週）
     start:    true              # すぐにcronのjobを実行するか
     timeZone: 'Asia/Tokyo'      # タイムゾーン指定
     onTick: ->                  # 時間が来た時に実行する処理
-      message = '時刻は ' + moment().format('YYYY-MM-DD HH:mm:ss');
+      message = moment().format('YYYY年M月D日 H時になりました。');
       robot.send {room: 'test'}, message
     )


Match multi line at goal script

Match multi line at goal script

--- scripts/goal.coffee
+++ scripts/goal.coffee
@@ -39,7 +39,7 @@
     "e74c3c"
   ]

-  robot.respond /今日の目標は(?!？)(.+)/i, (msg) ->
+  robot.respond /今日の目標は(?!？)([\s\S]+)/i, (msg) ->
     goal = msg.match[1]
     user = msg.envelope.user.name
     expiration = moment()


Work around weird edge case for dequeueing uploads

--- scripts/home.coffee
+++ scripts/home.coffee
@@ -31,6 +31,8 @@
     window.statusChange = (file, status, oldStatus) ->
         if oldStatus == 'uploading'
             uploadPendingItems()
+    # We kick this manually every so often to make sure nothing gets abandoned in the 'Pending' state
+    setInterval(uploadPendingFiles, 10000)

     compile = (name) -> Handlebars.compile(document.getElementById(name + '-template').innerHTML)
     templates.preview = compile 'preview'
@@ -190,9 +192,7 @@
     for hash, file of uploadedFiles
         if file.status in ['preparing', 'uploading']
             uploading++
-            if uploading >= maxConcurrentUploads
-                setTimeout(uploadPendingFiles, 1000)
-                return
+            return if uploading >= maxConcurrentUploads
         else if file.status == 'local-pending' and toUpload.length < 5
             toUpload.push(file)
     for file in toUpload


Added filter config which was missing while making pivotUIOptions

--- pivot.coffee
+++ pivot.coffee
@@ -669,6 +669,7 @@
                 hiddenAttributes: opts.hiddenAttributes
                 renderers: opts.renderers
                 aggregators: opts.aggregators
+                filter: opts.filter
                 derivedAttributes: opts.derivedAttributes
                 aggregatorName: aggregator.val()
                 rendererName: renderer.val()


Apply code rules

Apply code rules

--- client/account/lib/accounttwofactorauth.coffee
+++ client/account/lib/accounttwofactorauth.coffee
@@ -1,8 +1,11 @@
-kd     = require 'kd'
-whoami = require 'app/util/whoami'
-
-
-module.exports = class AccountTwoFactorAuth extends kd.View
+kd                  = require 'kd'
+whoami              = require 'app/util/whoami'
+KDView              = kd.View
+KDButtonView        = kd.ButtonView
+
+
+module.exports = class AccountTwoFactorAuth extends KDView
+

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

@@ -82,7 +85,7 @@
         button             :
           label            : ' '
           cssClass         : 'Formline--half'
-          itemClass        : kd.ButtonView
+          itemClass        : KDButtonView
           title            : 'Disable 2-Factor Auth'
           style            : 'solid medium disable-tf'
           callback         : @bound 'handleDisableFormButton'


Always fetchFromDB first then when it fails try to fetch from FS

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -70,8 +70,6 @@
       if not @appStorage.getValue 'shortcuts'
         @putDefaultShortcutsToAppStorage()

-    @fetchedFromFs = no
-
   # #
   # FETCHERS
   # #
@@ -81,20 +79,14 @@
     if Object.keys(@constructor.manifests).length isnt 0
       callback null, @constructor.manifests
     else
-      if not @fetchedFromFs
-        @fetchAppsFromFs (err, apps)=>
-          if err then callback()
-          else
-            callback null, apps
-      else
-        @fetchAppsFromDb (err, apps)=>
-          if err
-            @fetchAppsFromFs (err, apps)=>
-              if err then callback()
-              else
-                callback null, apps
-          else
-            callback? err, apps
+      @fetchAppsFromDb (err, apps)=>
+        if err
+          @fetchAppsFromFs (err, apps)=>
+            if err then callback()
+            else
+              callback null, apps
+        else
+          callback? err, apps

   fetchAppsFromFs:(callback)->

@@ -146,7 +138,6 @@

           @putAppsToAppStorage manifests
           callback? null, manifests
-          @fetchedFromFs = yes

   fetchAppsFromDb:(callback)->

@@ -259,7 +250,7 @@
   runApp:(manifest, callback)->

     {options, name, devMode} = manifest
-    {stylesheets}   = manifest.source if manifest.source
+    {stylesheets} = manifest.source if manifest.source

     if stylesheets
       stylesheets.forEach (sheet)->


Fix date filters

Fix date filters

--- client/app/components/date_range_picker.coffee
+++ client/app/components/date_range_picker.coffee
@@ -38,15 +38,15 @@
     onStartChange: (obj) ->
         date = if obj.target? then obj.target.value else
             "#{obj.dd}/#{obj.mm}/#{obj.yyyy}"
-        active: !!date and !!@state.endDate
+        active = !!date and !!@state.endDate
         @setState isActive: active, startDate: date, @filterize

     onEndChange: (obj) ->
         date = if obj.target then obj.target.value else
             "#{obj.dd}/#{obj.mm}/#{obj.yyyy}"
-        active: !!@state.startDate and !!date
-        @setState isActive: endDate: date, @filterize
+        active = !!@state.startDate and !!date
+        @setState isActive: active, endDate: date, @filterize

     filterize: ->


Environments: Actions: Wait for ready state of compute controller

--- client/app/lib/flux/environment/actions.coffee
+++ client/app/lib/flux/environment/actions.coffee
@@ -58,18 +58,18 @@

   { reactor, computeController } = kd.singletons

-  computeController.on 'StackRevisionChecked', (stack) ->
-
-    return  if _revisionStatus?.error? and not stack._revisionStatus.status
-
-    loadMachines().then ->
-      reactor.dispatch actions.STACK_UPDATED, stack
-
-  computeController.on 'GroupStacksInconsistent', ->
-    reactor.dispatch actions.GROUP_STACKS_INCONSISTENT
-
-  computeController.on 'GroupStacksConsistent', ->
-    reactor.dispatch actions.GROUP_STACKS_CONSISTENT
+  computeController.ready ->
+    computeController.on 'StackRevisionChecked', (stack) ->
+      return  if _revisionStatus?.error? and not stack._revisionStatus.status
+
+      loadMachines().then ->
+        reactor.dispatch actions.STACK_UPDATED, stack
+
+    computeController.on 'GroupStacksInconsistent', ->
+      reactor.dispatch actions.GROUP_STACKS_INCONSISTENT
+
+    computeController.on 'GroupStacksConsistent', ->
+      reactor.dispatch actions.GROUP_STACKS_CONSISTENT

 handleSharedMachineInvitation = (sharedMachine)->


styleguide: Matched if-then-else guide

styleguide: Matched if-then-else guide

--- client/app/lib/providers/computehelpers.coffee
+++ client/app/lib/providers/computehelpers.coffee
@@ -36,8 +36,9 @@
     # not return a promise.
     destroyPromises.push new Promise (resolve, reject) ->
       machineCallback = (err) ->
-        return reject err  if err
-        resolve()
+        if err
+        then reject err
+        else resolve()
       ComputeHelpers.destroyExistingMachines waitForCompleteDeletion, machineCallback

     # Add the destroy Snapshots promise
@@ -95,8 +96,9 @@
     # Fetch snapshots
     result = new Promise (resolve, reject) ->
       JSnapshot.some {}, {}, (err, snapshots) ->
-        return reject err  if err
-        resolve snapshots
+        if err
+        then reject err
+        else resolve snapshots

     # call deleteSnapshot for all snapshots, and return a promise
     result = result.then (snapshots = []) ->


Improved email check and shortened code

Improved email check and shortened code

* A regexp is used to determine if an input is an email address;
* As explained in one of your note @gregorylegarec I shortened the code
by using '_.extend' and ternary operators.

Signed-off-by: Julien Loudet <5c682c2d1ec4073e277f9ba9f4bdf07e5794dabe@cozycloud.cc>

--- client/app/views/popover_screens/guests.coffee
+++ client/app/views/popover_screens/guests.coffee
@@ -181,83 +181,66 @@

         # Determine if guest's "channel" of communication is the url of his cozy
         # or his mail address.
-        if (channel.indexOf "@") < 0
+        # The regular expression below was found here:
+        # http://www.regular-expressions.info/email.html
+        emailRegExp =
+            /// ^
+                (?=[A-Z0-9][A-Z0-9@._%+-]{5,253}$)[A-Z0-9._%+-]{1,64}
+                @
+                (?:(?=[A-Z0-9-]{1,63}\.)[A-Z0-9]+(?:-[A-Z0-9]+)*\.){1,8}
+                [A-Z]{2,63}$
+            ///i # the "i" is for case insensitive search
+
+        if emailRegExp.test channel
+            email = channel
+        else
             cozy  = channel
-        else
-            email = channel
-            # An empty value should not be submitted.
-            email = email.trim()
-
-        if email?.length > 0 or cozy?.length > 0
-            guests = @formModel.get('attendees') or []
-
-            # Look for a duplicate:
-            # * another guest with the same email;
-            if email?
-                guestBisEmail = _.findWhere(guests, email: email)
-            # * another guest with the same cozy;
-            if cozy?
-                guestBisCozy  = _.findWhere(guests, cozy: cozy)
-
-            # But a duplicate is not a duplicate under certain circumstances:
-            # * same email and `shareWithCozy` is true;
-            # * same cozy and `shareWithCozy` is false;
-            # In those cases we can add the new guest.
-            if (email? and (not guestBisEmail? or
-            guestBisEmail?.shareWithCozy)) or (cozy? and (not guestBisCozy? or
-            (not guestBisCozy?.shareWithCozy)))
-                newGuest =
-                    key       : random.randomString()
-                    status    : 'INVITATION-NOT-SENT'
-                    contactid : contactID
-
-                # If guest was "autocompleted" then contactID is not null. We
-                # can fill additionnal information.
-                if contactID?
-                    contact       = app.contacts.get contactID
-                    newGuest.name = contact.get 'name'
-                    if email?
-                        newGuest.email         = email
-                        newGuest.label         = email
-                        # By default if a guest has several cozy linked to him,
-                        # the first one is chosen.
-                        newGuest.cozy          =
-                            (contact.get 'cozy')?[0]?.value or null
-                        newGuest.shareWithCozy = false
-                    else if cozy?
-                        emails                 = (contact.get 'emails')
-                        # By default if a guest has several emails linked to
-                        # him, the first one is chosen.
-                        newGuest.email         = emails?[0]?.value or null
-                        newGuest.label         = cozy
-                        newGuest.cozy          = cozy
-                        newGuest.shareWithCozy = true
-                # Guest was manually entered
-                else
-                    if email?
-                        newGuest.label         = email
-                        newGuest.email         = email
-                        newGuest.shareWithCozy = false
-                        newGuest.cozy          = null
-                    else
-                        newGuest.label         = cozy
-                        newGuest.email         = null
-                        newGuest.shareWithCozy = true
-                        newGuest.cozy          = cozy
-
-                # Clone the source array, otherwise it's not considered as
-                # changed because it changes the model's attributes
-                guests = _.clone guests
-                guests.push newGuest
-                @formModel.set 'attendees', guests
-
-                # Inefficient way to refresh the list, but it's okay since
-                # it will never be a big list.
-                @render()
+            cozy  = cozy.trim()

         # Reset form field.
         @$('input[name="guest-name"]').val ''
         @$('input[name="guest-name"]').focus()
+
+        guests = @formModel.get('attendees') or []
+
+        # Look for a duplicate:
+        # * another guest with the same email and for whom the event is not
+        # shared;
+        if email? and (email.length > 0)
+            guestBisEmail = _.findWhere(guests, {email: email, share: false})
+        # * another guest with the same cozy and for whom the event is shared;
+        if cozy? and (cozy.length > 0)
+            guestBisCozy  = _.findWhere(guests, {cozy: cozy, share: true})
+
+        # If there is no duplicate we can add the guest.
+        if (email? and (email.length > 0) and (not guestBisEmail)) or
+        (cozy? and (cozy.length > 0) and (not guestBisCozy))
+            newGuest =
+                key       : random.randomString()
+                status    : 'INVITATION-NOT-SENT'
+                contactid : contactID
+
+            if contactID?
+                contact = app.contacts.get contactID
+
+            _.extend newGuest,
+                name  : if contact? then (contact.get 'name') else null
+                cozy  : cozy or
+                    (contact and contact.get('cozy')?[0]?.value) or null
+                email : email or
+                    (contact and contact.get('emails')?[0]?.value) or null
+                label : email or cozy
+                share : cozy?
+
+            # Clone the source array, otherwise it's not considered as
+            # changed because it changes the model's attributes
+            guests = _.clone guests
+            guests.push newGuest
+            @formModel.set 'attendees', guests
+
+            # Inefficient way to refresh the list, but it's okay since
+            # it will never be a big list.
+            @render()

     onKeyup: (event) ->


Respond in pair talk.

Respond in pair talk.

--- scripts/ping.coffee
+++ scripts/ping.coffee
@@ -30,24 +30,24 @@
              ・「+1」「-0.75」で時差を考慮します。
              """

-  robot.hear /^https:\/\/drive.google.com\/open/, (res) ->
+  robot.hear /^(Hubot )?https:\/\/drive.google.com\/open/i, (res) ->
     res.send "ブラウザで開いたときの https://docs.google.com/spreadsheets/... でお願いします。"

-  robot.hear /^(https:\/\/docs.google.com\/spreadsheets\/.*\/edit)/, (res) ->
-    robot.brain.set("sheet-url-#{res.message.user.id}", res.match[1])
+  robot.hear /^(Hubot )?(https:\/\/docs.google.com\/spreadsheets\/.*\/edit)/i, (res) ->
+    robot.brain.set("sheet-url-#{res.message.user.id}", res.match[2])
     res.send """
              #{res.message.user.name}さんの勤怠管理のURLを覚えました。
              共有設定で #{botEmail} を編集者として登録しておいてください
              """

-  robot.hear /^([+-＋−][0-9.]+)/, (res) ->
-    robot.brain.set("timeshift-#{res.message.user.id}", res.match[1])
+  robot.hear /^(Hubot )?([+-＋−][0-9.]+)/i, (res) ->
+    robot.brain.set("timeshift-#{res.message.user.id}", res.match[2])
     res.send "#{res.message.user.name}さんの勤怠管理の時差を覚えました。"

-  robot.hear /^(おはよう|お早う)/, (res) ->
+  robot.hear /^(Hubot )?(おはよう|お早う)/, (res) ->
     updateSheet res, "inTime"

-  robot.hear /^(おつかれ|お疲れ)/, (res) ->
+  robot.hear /^(Hubot )?(おつかれ|お疲れ)/, (res) ->
     updateSheet res, "outTime"

   robot.hear "stamp", (res) ->


kontrol: now can validate requests and make queries based on the incoming KontrolQuery struct

--- client/landing/app/MainApp/kite/kontrol.coffee
+++ client/landing/app/MainApp/kite/kontrol.coffee
@@ -4,7 +4,7 @@

   [NOTREADY, READY, CLOSED] = [0,1,3]

-  kontrolEndpoint = "http://#{KD.config.newkontrol.host}:#{KD.config.newkontrol.port}/request"
+  kontrolEndpoint = "http://#{KD.config.newkontrol.host}:#{KD.config.newkontrol.port}/query"

   constructor: (options)->
     super
@@ -13,9 +13,10 @@
     @connect()

   getKites: (kitename, callback)->
+    # find kites that belongs to username.
     queryData =
       username       : "#{KD.nick()}"
-      kitename       : kitename
+      name           : kitename
       authentication :
         type         : "browser"
         key          : KD.remote.getSessionToken()


Fixing polling so that it will still occur even if no data is fetched at the beginning

--- public/scripts/collections/SensorObservations.coffee
+++ public/scripts/collections/SensorObservations.coffee
@@ -6,17 +6,11 @@
   idAttribute: '_id'

   ###
-  Perform an initial fetch and then get the latest data every <interval>
-  milliseconds
-  ###
-  poll: (interval = 5000) -> @fetch().always => @repeat interval
-
-  ###
   Repeatedly poll the server for more date after a given interval
   ###
-  repeat: (after) -> _.delay (=> @more().always => @repeat after), after
+  poll: (after = 5000) -> _.delay (=> @more().always => @poll after), after

   ###
   Get the latest set of data from the end point
   ###
-  more: -> @fetch remove: false, data: since: @last().get 'timestamp'+  more: -> @fetch remove: false, data: since: @last()?.get 'timestamp'


removed duplicated method call and unnecessary pauses

--- client/test/lib/helpers/activityhelpers.coffee
+++ client/test/lib/helpers/activityhelpers.coffee
@@ -62,25 +62,21 @@

   editAction: (browser, type, editWithCode = yes, editWithImage = yes, editWithLink = yes) ->

+    @goToMessagesAndCommentSection(browser)
+
     if editWithCode
-      @goToMessagesAndCommentSection(browser)
-
       if type is 'message'
         @editMessageAction(browser, yes, no, no)
       else
         @editCommentAction(browser, yes, no, no)

     if editWithImage
-      @goToMessagesAndCommentSection(browser)
-
       if type is 'message'
         @editMessageAction(browser, no, yes, no)
       else
         @editCommentAction(browser, no, yes, no)

     if editWithLink
-      @goToMessagesAndCommentSection(browser)
-
       if type is 'message'
         @editMessageAction(browser, no, no, yes)
       else
@@ -174,9 +170,7 @@

   goToMessagesAndCommentSection: (browser) ->
     browser
-      .pause                    2500 # while typing something steals activity input focus
       .click                    '[testpath="public-feed-link/Activity/Topic/public"]'
-      .pause                    3000 # for page load
       .waitForElementVisible    '[testpath=ActivityInputView]', 30000
       .click                    '[testpath="ActivityTabHandle-/Activity/Public/Recent"] a'
       .waitForElementVisible    '.most-recent [testpath=activity-list]', 30000


incomplete

incomplete

--- coffee/hyperbolic_tessellation_test_app.coffee
+++ coffee/hyperbolic_tessellation_test_app.coffee
@@ -119,6 +119,7 @@
     for cell, i in @cells
       unless cells.get cell
         runCommands context, @cellShapes[i]
+        null
     context.stroke()

     #then cells
@@ -126,6 +127,7 @@
     for cell, i in @cells
       if cells.get cell
         runCommands context, @cellShapes[i]
+        null
     context.fill()
     return true

@@ -328,7 +330,9 @@

 drawEverything = ->
   s = Math.min( canvas.width, canvas.height ) / 2 #
-  context.clearRect 0, 0, canvas.width, canvas.height
+  context.fillStyle="white"
+  #context.clearRect 0, 0, canvas.width, canvas.height
+  context.fillRect 0, 0, canvas.width, canvas.height
   context.save()
   context.scale s, s
   context.translate 1, 1
@@ -337,13 +341,17 @@
   return rval

 lastTime = Date.now()
-fpsMax = 60
-dtMax = 1000.0/fpsMax #
+fpsDefault = 30
+dtMax = 1000.0/fpsDefault #
+
 redrawLoop = ->
   if dirty
     t = Date.now()
     if t - lastTime > dtMax
       if drawEverything()
+        tEnd = Date.now()
+        #adaptively update FPS
+        dtMax = dtMax*0.9 + (tEnd - t)*1.2*0.1
         dirty = false
       lastTime = t
   requestAnimationFrame redrawLoop


FIX: issue with updating tasks in the portfolio task selector.

--- src/app/projects/partials/directives/project-portfolio.coffee
+++ src/app/projects/partials/directives/project-portfolio.coffee
@@ -105,7 +105,7 @@

     includeTaskInPorfolio = (task, shouldInclude) ->
       task.include_in_portfolio = shouldInclude
-      Task.update { id: task.id, include_in_portfolio: task.include_in_portfolio },
+      Task.update { project_id: $scope.project.project_id, task_definition_id: task.definition.id, include_in_portfolio: task.include_in_portfolio },
         (success) ->
           task.include_in_portfolio = success.include_in_portfolio
 )


Export TYPE

Export TYPE

--- coffee/equ.coffee
+++ coffee/equ.coffee
@@ -1,5 +1,6 @@
 equ = require './lib/equ'
 token = require './lib/token'
+type = require './lib/type'
 {read} = require './lib/reader'
 {parse} = require './lib/parser'

@@ -8,5 +9,6 @@
   isEqu: equ.isEqu
   token: token.token
   isToken: token.isToken
+  TYPE: type.TYPE
   readAndParse: (path) -> equ.equ parse read path
   parse: (code) -> equ.equ parse code


[worker][social][model][activity] added client as an option to request parameter

--- workers/social/lib/social/models/activity/index.coffee
+++ workers/social/lib/social/models/activity/index.coffee
@@ -420,6 +420,7 @@

       neo4jConfig = KONFIG.neo4j
       requestOptions =
+        client    : client
         startDate : startDate
         neo4j     : neo4jConfig
         group     :


activityticker: add selector for the data.group field

--- workers/social/lib/social/models/activityticker.coffee
+++ workers/social/lib/social/models/activityticker.coffee
@@ -39,8 +39,6 @@
       return callback err if err
       source.tags = tags
       callback null, {source, target, as, timestamp}
-
-

   decorateLikeEvent = (relationship, callback) ->
     {source, target, as, timestamp} = relationship


payment: clean up failed payment methods

destroy the payment method object when errors occur during validation of new payment method

--- workers/social/lib/social/models/payment/method.coffee
+++ workers/social/lib/social/models/payment/method.coffee
@@ -46,7 +46,9 @@
       return callback err  if err

       paymentMethod.associatePaymentData client, data, (err) ->
-        return callback err  if err
+        if err
+          paymentMethod.remove()
+          return callback err
         callback null, paymentMethod

   @create = secure (client, formData, callback) ->
@@ -55,8 +57,9 @@
     @createPaymentMethod client, formData, (err, paymentMethod) ->
       return callback err  if err

-      delegate.addPaymentMethod paymentMethod, (err)->
+      delegate.addPaymentMethod paymentMethod, (err) ->
         return callback err  if err
+
         callback null, paymentMethod

   associatePaymentData: secure (client, formData, callback) ->


fix: error discovered during refactor

fix: error discovered during refactor

--- observer.coffee
+++ observer.coffee
@@ -162,7 +162,7 @@
     bestDifference = null

     angleOffsets = [0.0]
-    angleOffsets.push Math.PI/2 if tessellation.group.n % 2 is 1
+    angleOffsets.push Math.PI/2 if @tessellation.group.n % 2 is 1
     for additionalAngle in angleOffsets
       for i in [0...2*@tessellation.group.n]
         angle = dAngle*i + additionalAngle


Give better error messages on common dev environment cases

When the favicon.ico isn't found, it's almost always because the
public folder hasn't been built, so this catches that case and gives a
warning message that will hopefully point new developers rightly.

--- server_setup.coffee
+++ server_setup.coffee
@@ -72,8 +72,11 @@

   setupProxyMiddleware app # TODO: Flatten setup into one function. This doesn't fit its function name.

-
-  app.use require('serve-favicon') path.join(publicPath, 'images', 'favicon', "favicon-#{productSuffix}", 'favicon.ico')
+  try
+    app.use require('serve-favicon') path.join(publicPath, 'images', 'favicon', "favicon-#{productSuffix}", 'favicon.ico')
+  catch e
+    console.error "Error. Couldn't find #{path.join(publicPath, 'images', 'favicon', 'favicon-' + productSuffix)}. It is likely that the #{publicFolderName} folder is not built. Try:\n\n  npm run build\n\nfor an initial build, or\n\n  npm run dev\n\nfor live rebuilding of your front-end changes. If those don't work, make sure you are running the correct version of node and have installed all dependencies with:\n\n  npm install --also=dev\n"
+    process.exit(1)
   app.use require('cookie-parser')()
   app.use require('body-parser').json limit: '25mb', strict: false
   app.use require('body-parser').urlencoded extended: true, limit: '25mb'


Properly validate 'Accept' header

Properly validate 'Accept' header

--- app.coffee
+++ app.coffee
@@ -436,7 +436,10 @@
 				res.redirect 301, "#{host}/#{req.query.packageName}"
 			return

-		req.accepts 'xml'
+		unless req.accepts 'xml'
+			res.status(406).send 'Not Acceptable'
+			return
+
 		res.type 'xml'

 		# build the xml structure of the package list


Add function for adding alerts

Add function for adding alerts

--- static/scripts/main.coffee
+++ static/scripts/main.coffee
@@ -14,6 +14,12 @@

 peer.on "error", (error) ->
     alert error.message
+
+Noop = React.createClass
+    displayName: "Noop"
+
+    render: ->
+        (Dom.span null)

 Input = React.createClass
     displayName: "Input"
@@ -234,6 +240,7 @@

         getInitialState: ->
             currentComponent: Connect()
+            alert: Noop()

         componentDidMount: ->
             _.forEach @props.routes, (route) =>
@@ -246,7 +253,11 @@
         render: ->
             (Dom.div null,
                 (Navbar brandName: "Peer Chat", navbarItems: @props.navbarItems),
+                (@state.alert)
                 (@state.currentComponent))

+        addAlert: (alert) ->
+            @setState alert: alert
+
     mountNode = document.getElementById("react")
-    React.renderComponent Root(routes: routes, navbarItems: navbarItems, defaultRoute: "/connect"), mountNode
+    React.renderComponent Root(routes: routes, defaultRoute: "/connect", navbarItems: navbarItems), mountNode


Add linux cmd-enter replace keybindings

Add linux cmd-enter replace keybindings

--- keymaps/find-and-replace.cson
+++ keymaps/find-and-replace.cson
@@ -59,3 +59,8 @@
   'ctrl-enter': 'find-and-replace:replace-all'
 '.platform-win32 .project-find .replace-container atom-text-editor':
   'ctrl-enter': 'project-find:replace-all'
+
+'.platform-linux .find-and-replace .replace-container atom-text-editor':
+  'ctrl-enter': 'find-and-replace:replace-all'
+'.platform-linux .project-find .replace-container atom-text-editor':
+  'ctrl-enter': 'project-find:replace-all'


Updated test/lib/test-utils.coffee (makeFileOfSizeInMBytesUnlessFileExists)

--- test/lib/test-utils.coffee
+++ test/lib/test-utils.coffee
@@ -3,7 +3,7 @@

 { iterate } = require './en-stl.coffee'

-@makeFileOfSizeInMBytes = (inputFilePath, sizeInMBytes, cbfn)->
+@makeFileOfSizeInMBytes = makeFileOfSizeInMBytes = (inputFilePath, sizeInMBytes, cbfn)->

   stream = fs.createWriteStream inputFilePath

@@ -19,6 +19,22 @@

   stream.on 'finish', =>
     cbfn()
+
+
+@makeFileOfSizeInMBytesUnlessFileExists = (inputFilePath, sizeInMBytes, cbfn)->
+
+  fs.stat inputFilePath, (err, stats)->
+    if err
+      if err.code is 'ENOENT'
+        return makeFileOfSizeInMBytes inputFilePath, sizeInMBytes, cbfn
+      else
+        throw err
+
+    if stats.size is sizeInMBytes * 1024 * 1024
+      return cbfn()
+    else
+      return makeFileOfSizeInMBytes inputFilePath, sizeInMBytes, cbfn
+

 hashFiles = require 'hash-files'


more stupid mistakes

more stupid mistakes

--- workers/vhostconfigurator/deletevhost.coffee
+++ workers/vhostconfigurator/deletevhost.coffee
@@ -1,5 +1,5 @@
 {error, execute} = require './helpers'

 module.exports = (vhost, config, callback)->
-  execute "rabbitmqctl delete_vhosts", (stdout)->
+  execute "rabbitmqctl delete_vhost #{vhost}", (stdout)->
     callback null


tests/logs.spec: Refactor to use the same app for all test cases

Change-type: patch
Signed-off-by: Thodoris Greasidis <5791bdc4541e79bd43d586e3c3eff1e39c16665f@balena.io>

--- tests/integration/logs.spec.coffee
+++ tests/integration/logs.spec.coffee
@@ -2,7 +2,12 @@
 m = require('mochainon')
 rindle = require('rindle')

-{ balena, sdkOpts, givenLoggedInUser } = require('./setup')
+{
+	balena
+	givenAnApplication
+	givenLoggedInUser
+	sdkOpts
+} = require('./setup')

 sendLogMessages = (uuid, deviceApiKey, messages) ->
 	balena.request.send
@@ -15,21 +20,21 @@

 describe 'Logs', ->

-	givenLoggedInUser(beforeEach)
+	givenLoggedInUser(before)

 	describe 'given a device', ->

+		givenAnApplication(before)
+
 		beforeEach ->
-			balena.models.application.create
-				name: 'FooBar'
-				deviceType: 'raspberry-pi'
-				applicationType: 'microservices-starter'
-			.then (application) =>
-				@application = application
-				@uuid = balena.models.device.generateUniqueKey()
-				balena.models.device.register(application.id, @uuid)
+			@uuid = balena.models.device.generateUniqueKey()
+			balena.models.device.register(@application.id, @uuid)
 			.then (registrationInfo) =>
 				@deviceApiKey = registrationInfo.api_key
+
+		afterEach ->
+			balena.pine.delete
+				resource: 'device'

 		describe 'balena.logs.history()', ->


MachineStateModal: follow upcoming events for given machine

--- client/ide/views/modals/machinestatemodal.coffee
+++ client/ide/views/modals/machinestatemodal.coffee
@@ -36,6 +36,7 @@

     @show()

+    computeController.followUpcomingEvents @machine

   updateStatus: (event) ->


:bug:Fix sub handler error typo

:bug:Fix sub handler error typo

--- server/payments/subscription_handler.coffee
+++ server/payments/subscription_handler.coffee
@@ -645,7 +645,7 @@
             stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
               if err
                 logStripeWebhookError(err)
-                return res.send(500, '')
+                return done({res: 'Database error.', code: 500})
               done()

 module.exports = new SubscriptionHandler()


Much better

Much better

--- client/landing/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
+++ client/landing/app/Applications/StartTab.kdapplication/views/appthumbview.coffee
@@ -21,8 +21,7 @@
     version        or= ''
     appPath          = ''

-    if not authorNick
-      authorNick = KD.whoami().profile.nickname
+    authorNick or= KD.nick()

     resourceRoot = "#{KD.appsUri}/#{authorNick}/#{identifier}/#{version}"


suppress all cascading chktex environment errors

--- services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
+++ services/web/public/coffee/ide/human-readable-logs/HumanReadableLogsRules.coffee
@@ -155,6 +155,7 @@
 	,
 		ruleId: "hint_mismatched_environment2"
 		types: ['environment']
+		cascadesFrom: ['environment']
 		regexToMatch: /Error: `\\end\{([^\}]+)\}' expected but found `\\end\{([^\}]+)\}'.*/
 		newMessage: "Error: environments do not match: \\begin{$1} ... \\end{$2}"
 		humanReadableHint: """
@@ -163,6 +164,7 @@
 	,
 		ruleId: "hint_mismatched_environment3"
 		types: ['environment']
+		cascadesFrom: ['environment']
 		regexToMatch: /Warning: No matching \\end found for `\\begin\{([^\}]+)\}'.*/
 		newMessage: "Warning: No matching \\end found for \\begin{$1}"
 		humanReadableHint: """


fix tests

fix tests

--- test/slim2html.spec.coffee
+++ test/slim2html.spec.coffee
@@ -45,8 +45,8 @@
     file = new File 'test/fixtures/invalid.slim'
     process = slim2html({}, logger)
     process '', file, (result) ->
-      expect(result).to.equal(null)
-      expect(logged.error).to.match(/SyntaxError: Malformed indentation/)
+      expect(result).to.equal('')
+      expect(logged.error).to.include("SyntaxError: Malformed indentation")
       done()

   it 'allows configuring the path of the slimrb binary', (done) ->
@@ -60,6 +60,8 @@
     file = new File 'test/fixtures/index.slim'
     process = slim2html({ slimrb: "foooooooooooo" }, logger)
     process '', file, (result) ->
-      console.log('done')
+      expect(result).to.equal('')
+      expect(logged.error).to.include("bundle install")
+      expect(logged.error).to.include("slimPreprocessor: { slimrb: '/usr/local/slimrb' }`")
       done()


Fix superfluous horizontal line

Fix superfluous horizontal line

--- Packages/package_test.coffee
+++ Packages/package_test.coffee
@@ -2,7 +2,7 @@
 e = require('./package-entry')

 describe('package_test', ->
-  before(() => global.ran.push('package'))
+  before(() -> global.ran.push('package'))
   it('exports accordingly', ->
     assert.equal(e.name, 'package-entry')
     assert.equal(e.rel.name, 'package-include')


Add a FIXME for someone who wants a low hanging fruit

--- bokehjs/test/test_mapper/test_categorical_mapper.coffee
+++ bokehjs/test/test_mapper/test_categorical_mapper.coffee
@@ -4,6 +4,7 @@
 {Collections} = utils.require "common/base"

 describe "categorical mapper", ->
+  # FIXME Would be nice to randomize the numbers
   factors = ["foo", "bar", "baz"]
   start = 20
   end = 80


Add background colour back in so matching is easier

--- app/assets/javascripts/lib/styleguide/colours.js.coffee
+++ app/assets/javascripts/lib/styleguide/colours.js.coffee
@@ -33,6 +33,7 @@

   resetProximity = ->
     document.getElementById("proximityMatch").value = ""
+    $(".styleguide__colours").removeAttr("style")
     $(".js-card-container .card").each ->
       @.removeAttribute "style"
       @.classList.remove('isnt-approximate')
@@ -41,10 +42,12 @@

   matchProximity = ->
     colour = document.getElementById("proximityMatch").value
+    sections = $(".styleguide__colours").removeClass("has-match").removeAttr("style")
+
     colourBlocks = $(".styleguide-block__item--colour")
     colourBlocks.each ->
       proximity = colourProximity(colour, @.innerHTML)
-
+
       # Remove previous classes
       @.parentNode.removeAttribute "style"
       @.parentNode.className = @.parentNode.className.replace(" is-approximate", "").replace(" isnt-approximate", "")
@@ -52,11 +55,17 @@
         @.style["border-color"] = (if /^#/.test(colour) then colour else "#" + colour)
         @.parentNode.className += " is-approximate"
         @.parentNode.className += " is-exact icon--tick--before"  if proximity is 0
+        $(@).closest(".styleguide__colours").addClass("has-match")
       else
         @.parentNode.className += " isnt-approximate"

+    sections.filter(".has-match").css
+      backgroundColor: "#"+colour.replace("#", "")
+      paddingRight: 10
+      paddingTop: 10

-  # Mostly ripped from here: http://stackoverflow.com/questions/13586999/color-difference-similarity-between-two-values-with-js
+
+  # Mostly ripped from here: http://stackoverflow.com/questions/13586999/color-difference-similarity-between-two-values-with-js
   colourProximity = (v1, v2) ->
     i = undefined
     d = 0
@@ -70,4 +79,4 @@

   hexToRGB = (colour) ->
     colour = colour.replace(/^#/, "").match(/[0-9a-z]{2}/gi)
-    [parseInt(colour[0], 16), parseInt(colour[1], 16), parseInt(colour[2], 16)]+    [parseInt(colour[0], 16), parseInt(colour[1], 16), parseInt(colour[2], 16)]


Call API after updating flexibility order

--- app/assets/javascripts/lib/views/base_chart_view.coffee
+++ app/assets/javascripts/lib/views/base_chart_view.coffee
@@ -98,7 +98,7 @@
           order: order,
           scenario_id: globals.api_session_id
       success: ->
-        console.log('Recalculate something')
+        App.call_api()
       error: (e,f) ->
         console.log('Throw error')


Added serialization of address id

Added serialization of address id

--- app/assets/javascripts/sprangular/models/address.coffee
+++ app/assets/javascripts/sprangular/models/address.coffee
@@ -19,6 +19,7 @@
     "#{@firstname} #{@lastname}"

   serialize: ->
+    id: @id
     firstname: @firstname
     lastname: @lastname
     address1: @address1


Add Ajax call in intervention input computation

--- app/assets/javascripts/backend/interventions.js.coffee
+++ app/assets/javascripts/backend/interventions.js.coffee
@@ -183,6 +183,13 @@
             options.success.call(this, data, status, request) if options.success?
             console.groupEnd()

+            if options['display_cost']
+              targettedElement = options['targetted_element']
+              parentBlock = $(targettedElement).closest('.nested-product-parameter')
+              quantity = $(parentBlock).find('input[data-intervention-field="quantity-value"]').val()
+              unitName = $(parentBlock).find('select[data-intervention-field="quantity-handler"]').val()
+              E.interventionForm.displayCost(targettedElement, quantity, unitName)
+
     hideKujakuFilters: (hideFilters) ->
       if hideFilters
         $('.feathers input[name*="nature"], .feathers input[name*="state"]').closest('.feather').hide()
@@ -313,32 +320,31 @@
   #  selector:initialized
   $(document).on 'selector:change', '*[data-intervention-updater]', (event) ->
     $(this).each ->
+      options = {}
+      options['display_cost'] = true
+      options['targetted_element'] = $(event.target)
+
+      E.interventions.refresh $(this), options
+
+  $(document).on 'keyup', 'input[data-selector]', (e) ->
+    $(this).each ->
       E.interventions.refresh $(this)

-      parentBlock = $(event.target).closest('.nested-product-parameter')
-      quantity = $(parentBlock).find('input[data-intervention-field="quantity-value"]').val()
-      unitName = $(parentBlock).find('select[data-intervention-field="quantity-handler"]').val()
-      E.interventionForm.displayCost(event.target, quantity, unitName)
-
-  $(document).on 'keyup', 'input[data-selector]', (e) ->
-    $(this).each ->
-      E.interventions.refresh $(this)
-
   $(document).on 'keyup change', 'input[data-intervention-updater]:not([data-selector])', (event) ->
     $(this).each ->
-      E.interventions.refresh $(this)
-
-      quantity = $(event.target).val()
-      unitName = $(event.target).parent().find('select[data-intervention-field="quantity-handler"]').val()
-      E.interventionForm.displayCost(event.target, quantity, unitName)
+      options = {}
+      options['display_cost'] = true
+      options['targetted_element'] = $(event.target)
+
+      E.interventions.refresh $(this), options

   $(document).on 'keyup change', 'select[data-intervention-updater]', (event) ->
     $(this).each ->
-      E.interventions.refresh $(this)
-
-      quantity = $(event.target).parent().find('input[data-intervention-field="quantity-value"]').val()
-      unitName = $(event.target).val()
-      E.interventionForm.displayCost(event.target, quantity, unitName)
+      options = {}
+      options['display_cost'] = true
+      options['targetted_element'] = $(event.target)
+
+      E.interventions.refresh $(this), options

   $(document).on "keyup change dp.change", ".nested-fields.working-period:first-child input.intervention-started-at", (e) ->
     $(this).each ->


remove unused broker class

remove unused broker class

--- scripts/permission-updater/main.coffee
+++ scripts/permission-updater/main.coffee
@@ -1,4 +1,3 @@
-Broker = require 'broker'
 Bongo = require 'bongo'
 { join: joinPath } = require 'path'

@@ -13,7 +12,6 @@
 koding = new Bongo
   root   : __dirname
   mongo  : mongo
-  # mq     : new Broker KONFIG.mq
   models : modelPath

 done = ->


in order to display user names, subscribe to all users on the instances page

--- routes/web.coffee
+++ routes/web.coffee
@@ -24,6 +24,9 @@

     @route 'instances',
       path: '/instances'
+      subscriptions: -> [
+        Meteor.subscribe 'allUsers'
+      ]

     @route 'instancesDetail',
       path: '/instances/:name'


Added test for cycles and undefined components

--- source/index.coffee
+++ source/index.coffee
@@ -1,52 +1,62 @@
-configure = ($dependencies) ->
-  IOC = {}
-  IOC.configure = configure
+configure = (configuration) ->

-  constructDependencies = (props = {}) ->
-    $ = Object.create props
+  class Configuration
+    constructor: (properties = {}) ->
+      @[key] = val for own key,val of properties

-    $.createPromise ?= (resolver) ->
-      require("when").promise resolver
+    require: (name) ->
+      require name

-    $.asyncMap ?= (array, mapFn, done) ->
-      require("async").map array, mapFn, done
+    createPromise: (resolver) ->
+      @require("when").promise resolver

-    $.asyncWaterfall ?= (series, done) ->
-      require("async").waterfall series, done
+    asyncMap: (array, mapFn, done) ->
+      @require("async").map array, mapFn, done

-    return $
+    asyncWaterfall: (series, done) ->
+      @require("async").waterfall series, done

-  $ = constructDependencies $dependencies
+  class CustomError extends Error
+    name: undefined

-  class IOC.ComponentDependenciesNotDefined extends Error
-    constructor: (props = {}) ->
+    constructor: (properties = {}) ->
+      @[key] = val for own key,val of properties
+
       @name = @constructor.name
-      @componentName = props.componentName
-      @dependenciesNotDefined = props.dependenciesNotDefined
-      super @getMessage()
       Error.captureStackTrace @, @constructor

-    getMessage: ->
+  class ComponentDependenciesUndefined extends CustomError
+    componentName: undefined
+    undefinedDependencies: undefined
+
+    Object.defineProperty @prototype, "message", get: ->
       "Component '#{@componentName}' " +
-      "has undefined dependencies: [#{@dependenciesNotDefined}]"
+      "has undefined dependencies: [#{@undefinedDependencies}]"

-  class IOC.ComponentNotAcyclic extends Error
-    constructor: (props = {}) ->
-      @name = @constructor.name
-      @componentName = props.componentName
-      @cycles = props.cycles
-      @message = @getMessage()
-      Error.captureStackTrace @, @constructor
+  class ComponentCyclic extends CustomError
+    componentName: undefined
+    cycles: undefined

-    getMessage: ->
-      message = "Component '#{@componentName}' has cycles:\n"
-      @cycles.forEach (cycle) ->
-        message += "*  #{cycle.join(' > ')}"
-      message
+    inspectCycles: ->
+      mapFn = (cycle) -> "* #{cycle.join(" > ")}"
+      @cycles.map(mapFn).join("\n")

-  class IOC.Dictionary
-    constructor: (props = {}) ->
-      @definitions = props.definitions ?= {}
+    Object.defineProperty @prototype, "message", get: ->
+      "Component '#{@componentName}' has cycles:\n" +
+      @inspectCycles()
+
+  class ComponentUndefined extends CustomError
+    componentName: undefined
+
+    Object.defineProperty @prototype, "message", get: ->
+      "Component not defined: '#{@componentName}'"
+
+  class Dictionary
+    @definitions: undefined
+
+    constructor: (properties = {}) ->
+      @[key] = val for own key,val of properties
+      @definitions ?= {}

     set: (name, value) ->
       @definitions[name] = value
@@ -60,82 +70,90 @@
     isUndefined: (name) ->
       @definitions[name] == undefined

-  class IOC.Container
-    createPromise: $.createPromise
+  class Components extends Dictionary

-    constructor: (props = {}) ->
-      @components = props.components ?= new IOC.Dictionary
-      @componentPromises = props.componentPromises ?= new IOC.Dictionary
+    getUndefinedDependencies: (name) ->
+      @get(name).dependencies.filter @isUndefined.bind(@)
+
+    getCycles: (name, stack = []) ->
+      return stack.concat(name) if stack.indexOf(name) isnt -1
+      return [] unless @isDefined(name)
+
+      reduceDependencyCycles = (memo, dependency) =>
+        cycles = @getCycles dependency, stack
+        memo.push cycles if cycles.length isnt 0
+        memo
+
+      stack = stack.concat name
+      dependencies = @get(name).dependencies
+      dependencies.reduce reduceDependencyCycles, []
+
+    assertDefined: (name) ->
+      return if @isDefined(name)
+      throw new ComponentUndefined
+        componentName: name
+
+    assertAcyclic: (name) ->
+      cycles = @getCycles name
+      return if cycles.length is 0
+      throw new ComponentCyclic
+        componentName: name
+        cycles: cycles
+
+    assertDependenciesDefined: (name) ->
+      undefinedDependencies = @getUndefinedDependencies name
+      return if undefinedDependencies.length is 0
+      throw new ComponentDependenciesUndefined
+        componentName: name
+        undefinedDependencies: undefinedDependencies
+
+    validate: (name) ->
+      @assertDefined name
+      @assertAcyclic name
+      @assertDependenciesDefined name
+
+  class Promises extends Dictionary
+
+  class Container
+    components: undefined
+    promises: undefined
+
+    constructor: (properties = {}) ->
+      @[key] = val for own key,val of properties
+
+      @components ?= new Components
+      @promises ?= new Promises

     createComponentPromise: (name) ->
-      component = @components.get name
-
-      @createPromise (resolve, reject) =>
-        component.resolve this, (error, result) ->
+      $.createPromise (resolve, reject) =>
+        @components.get(name).resolve this, (error, result) ->
           return reject error if error?
           return resolve result

-    ensureComponentPromise: (name) ->
-      return if @componentPromises.isDefined name
-      @componentPromises.set name, @createComponentPromise(name)
-
-    getComponentDependenciesNotDefined: (name) ->
-      component = @components.get name
-      component.dependencies.filter @components.isUndefined.bind(@components)
-
-    assertComponentDependenciesDefined: (name) ->
-      dependenciesNotDefined = @getComponentDependenciesNotDefined name
-      return if dependenciesNotDefined.length is 0
-
-      throw new IOC.ComponentDependenciesNotDefined \
-        componentName: name
-        dependenciesNotDefined: dependenciesNotDefined
-
-    findComponentCycles: (name) ->
-      stack = []
-      cycles = []
-
-      stackContains = (value) ->
-        stack.some (name) -> name is value
-
-      visitComponent = (name) =>
-        return cycles.push(stack.concat(name)) if stackContains(name)
-        stack.push name
-        dependencies = @components.get(name).dependencies
-        dependencies.forEach visitComponent
-        stack.pop()
-
-      visitComponent name
-      return cycles
-
-    assertComponentIsAcyclic: (name) ->
-      cycles = @findComponentCycles name
-      return if cycles.length is 0
-      throw new IOC.ComponentNotAcyclic
-        componentName: name
-        cycles: cycles
-
-    validateComponent: (name) ->
-      return if @componentPromises.isDefined name
-      @assertComponentDependenciesDefined name
-      @assertComponentIsAcyclic name
+    ensureComponentPromiseSet: (name) ->
+      return if @promises.isDefined name
+      @promises.set name, @createComponentPromise(name)
+      @promises.get name

     resolveComponent: (name, done) ->
-      @validateComponent name
-      @ensureComponentPromise name
-      @componentPromises.get(name).then done.bind(null, null), done
+      try
+        @components.validate(name) unless @promises.isDefined(name)
+        @ensureComponentPromiseSet(name).then done.bind(null, null), done
+      catch error then done error

-  class IOC.Component
-    asyncMap: $.asyncMap
-    asyncWaterfall: $.asyncWaterfall
+  class Component
+    dependencies: undefined
+    factory: undefined

-    constructor: (props = {}) ->
-      @dependencies = props.dependencies ? []
-      @factory = props.factory
+    constructor: (properties = {}) ->
+      @[key] = val for own key,val of properties
+
+      @dependencies ?= []
+      @factory ?= null

     resolveDependencies: (container, done) ->
       resolveComponent = container.resolveComponent.bind container
-      @asyncMap @dependencies, resolveComponent, done
+      $.asyncMap @dependencies, resolveComponent, done

     applyFactory: (args, done) ->
       @factory args..., done
@@ -145,9 +163,22 @@
         @resolveDependencies.bind(this, container),
         @applyFactory.bind this

-      @asyncWaterfall series, done
+      $.asyncWaterfall series, done

+  $ = new Configuration configuration

-  return IOC
+  IOC =
+    configuration: $
+    configure: configure
+    Configuration: Configuration
+    CustomError: CustomError
+    ComponentUndefined: ComponentUndefined
+    ComponentDependenciesUndefined: ComponentDependenciesUndefined
+    ComponentCyclic: ComponentCyclic
+    Dictionary: Dictionary
+    Components: Components
+    Promises: Promises
+    Container: Container
+    Component: Component

 module.exports = configure()


Handled preprocessing errors

Handled preprocessing errors

--- client/import.coffee
+++ client/import.coffee
@@ -151,6 +151,7 @@
   document = ImportingFile.documents.findOne
     sha256:
       $exists: false
+    errored: false
   ,
     fields:
       _id: 1
@@ -176,6 +177,12 @@
           $set:
             sha256: sha256
             status: "In import queue"
+  reader.onerror = (error) ->
+    console.log "Error " + error
+    ImportingFile.documents.update document._id,
+      $set:
+        errored: true
+        status: error.toString()

   # TODO: Read file in chunks
   reader.readAsArrayBuffer importingFiles[document._id]