Resequence step number.

Resequence step number.

Now the first .toBeRevealed panel step will be 0.

--- app/js/lessons.coffee
+++ app/js/lessons.coffee
@@ -41,7 +41,7 @@
   nextSection = $(".toBeRevealed").first()
   if (nextSection?)
     nextStepId = nextSection.data("step")
-    currStepId = $(currentPad).closest(".displayed").data("step") ? 0
+    currStepId = $(currentPad).closest(".displayed").data("step") ? -1

     # Don't display the panel unless it is the next one
     return unless currStepId + 1 == nextStepId
@@ -54,7 +54,7 @@
 $(document).ready ->
   lessonNumber = window.location.pathname.split( '/' ).pop()

-  $(".toBeRevealed").each (idx, el) -> $(this).data("step", idx + 1)
+  $(".toBeRevealed").each (idx, el) -> $(this).data("step", idx)

   $(".scratchpad").each (idx, el) ->
     setupAce(lessonNumber, idx, el)


Removing parameter check for optional variable

--- server/methods/loadHistory.coffee
+++ server/methods/loadHistory.coffee
@@ -3,8 +3,8 @@

 		check rid, String
 		# check end, Match.Optional(Number)
-		check limit, Number
-		check ls, Match.Optional(Date)
+		# check limit, Number
+		# check ls, Match.Optional(Date)

 		if not Meteor.userId()
 			throw new Meteor.Error 'error-invalid-user', 'Invalid user', { method: 'loadHistory' }


fix diffmatchpatch reference

fix diffmatchpatch reference

--- sync.coffee
+++ sync.coffee
@@ -755,9 +755,9 @@
             return false

     constructor: (@app_id, @options) ->
-        @bversion = 2014022801
+        @bversion = 2014030401
         @jd = new jsondiff()
-        @dmp = jsondiff.dmp
+        @dmp = @jd.dmp
         @auth_token = null
         @options = @options || {}


Bugfix: rewired event listeners on ajax success

--- app/assets/javascripts/grade-editor.js.coffee
+++ app/assets/javascripts/grade-editor.js.coffee
@@ -63,6 +63,11 @@
 GradeEditor.onAjaxSuccess = (event, data, status, xhr) ->
   container = $(event.target).parent()
   container.html data
+  input = container.find "input[type=number]"
+  # hacky thing
+  input.on('blur', GradeEditor.onBlur)
+    .on('focus', GradeEditor.onFocus)
+    .on('keydown', GradeEditor.onKeyDown)
   indicators = $ '.progress-indicators', container
   GradeEditor.setIndicator indicators, 'upload-win', 1000

@@ -100,15 +105,18 @@

 # Wires event listeners into the DOM
 GradeEditor.onLoad = ->
-  $('table.grades-table input[type=number]').on 'blur', GradeEditor.onBlur
-  $('table.grades-table input[type=number]').on 'focus', GradeEditor.onFocus
-  $('table.grades-table input[type=number]').on 'keydown', GradeEditor.onKeyDown
-  $('table.grades-table input[type=search]').on 'change', GradeEditor.onSearchChange
-  $('table.grades-table input[type=search]').on 'textInput', GradeEditor.onSearchChange
-  $('table.grades-table input[type=search]').on 'input', GradeEditor.onSearchChange
-  $('table.grades-table input[type=search]').on 'keydown', GradeEditor.onSearchChange
-  $('table.grades-table form').on 'ajax:success', GradeEditor.onAjaxSuccess
-  $('table.grades-table form').on 'ajax:error', GradeEditor.onAjaxError
+  $('table.grades-table input[type=number]')
+    .on('blur', GradeEditor.onBlur)
+    .on('focus', GradeEditor.onFocus)
+    .on('keydown', GradeEditor.onKeyDown)
+  $('table.grades-table input[type=search]')
+    .on('change', GradeEditor.onSearchChange)
+    .on('textInput', GradeEditor.onSearchChange)
+    .on('input', GradeEditor.onSearchChange)
+    .on('keydown', GradeEditor.onSearchChange)
+  $('table.grades-table form')
+    .on('ajax:success', GradeEditor.onAjaxSuccess)
+    .on('ajax:error', GradeEditor.onAjaxError)
   $('table.grades-table tbody tr').each (index, row) ->
     GradeEditor.redoSummary row


Add draft for hack stopping multiple requests.

--- app/assets/javascripts/routes/index.js.coffee
+++ app/assets/javascripts/routes/index.js.coffee
@@ -3,6 +3,7 @@
 Wheelmap.IndexRoute = Ember.Route.extend
   boundsRatioBuffer: 0.41
   _previousBounds: null
+  _nodeRequestCounter: 0 # stop overlapping requests

   setupController: (controller, model, queryParams)->
     properties = {}
@@ -48,16 +49,21 @@
         @send('updateNodes', bounds)

     updateNodes: (bounds)->
+      self = @
+      currentRequestCount = ++self._nodeRequestCounter
+      self._previousBounds = bounds
+
       mapController = @controllerFor('map')
-      @_previousBounds = bounds
-
       mapController.set('isLoading', true)

       @store.findQuery('node', bbox: bounds.toBBoxString()).then (nodes)->
+        if currentRequestCount != self._nodeRequestCounter
+          return
+
         mapController.clear()
         mapController.addObjects(nodes)
-
         mapController.set('isLoading', false)
+        self._nodeRequestCounter = 0

     popping: ()->
       @send('permalink')


added `id` property again

added `id` property again

--- build/stations.coffee
+++ build/stations.coffee
@@ -17,6 +17,7 @@
 stations = {}
 set.on 'data', (data) ->
 	stations['' + data.id] =
+		id:			data.id
 		name:		data.name
 		weight:		data.weight
 		relevance:	0


documentation cleanup so that Intellij documentation lookup shows the docs. Makes browsing code easier.

Conflicts:
	term.coffee

--- term.coffee
+++ term.coffee
@@ -5,12 +5,24 @@
 ###

 $rdf = {} unless $rdf?
+
+###
+  the superclass of all RDF Statement objects, that is
+  $rdf.Symbol, $rdf.Literal, $rdf.BlankNode
+  No class extends this yet, but it could be a place to put common behavior.
+###
+class $rdf.Node

 class $rdf.Empty
     termType: 'empty'
     toString: -> '()'
     toNT: @::toString

+###
+   an RDF URI
+    todo: badly named.
+    @param uri the uri as string
+###
 class $rdf.Symbol
     constructor: (@uri) ->
         @value = @uri   # why?
@@ -116,6 +128,11 @@
 $rdf.Collection::sameTerm = $rdf.BlankNode::sameTerm
 $rdf.Collection::compareTerm = $rdf.BlankNode::compareTerm

+###
+ function to transform a value into an $rdf.Node
+ @param val can be an rdf.Node, a date, string, number, boolean, or undefined. RDF Nodes are returned as is,
+   undefined as undefined
+###
 $rdf.term = (val) ->
     switch typeof val
         when 'object'
@@ -198,6 +215,7 @@
         new $rdf.Formula
     collection: ->
         new $rdf.Collection
+    #todo: this is really badly named. It suggests that it creates a list, when in fact it creates a collection
     list: (values) ->
         r = new $rdf.Collection
         if values
@@ -208,10 +226,13 @@
     ns: (nsuri) ->
         (ln) -> new $rdf.Symbol(nsuri + (ln ? ''))

+    ###
+    transform an NTriples string format into an $rdf.Node
+    The bnode bit should not be used on program-external values; designed
+    for internal work such as storing a bnode id in an HTML attribute.
+    This will only parse the strings generated by the vaious toNT() methods.
+    ###
     fromNT: (str) ->
-        # The bnode bit should not be used on program-external values; designed
-        # for internal work such as storing a bnode id in an HTML attribute.
-        # This will only parse the strings generated by the vaious toNT() methods.
         switch str[0]
             when '<'
                 return $rdf.sym str[1...-1]
@@ -321,13 +342,15 @@
             done[t] = agenda[t]
             delete agenda[t]

+    ###
+    For thisClass or any subclass, anything which has it is its type
+    or is the object of something which has the type as its range, or subject
+    of something which has the type as its domain
+    We don't bother doing subproperty (yet?)as it doesn't seeem to be used much.
+    Get all the Classes of which we can RDFS-infer the subject is a member
+    @returns a hash of URIs
+    ###
     findMembersNT: (thisClass) ->
-        # For thisClass or any subclass, anything which has it is its type
-        # or is the object of something which has the type as its range, or subject
-        # of something which has the type as its domain
-        # We don't bother doing subproperty (yet?)as it doesn't seeem to be used much.
-        # Get all the Classes of which we can RDFS-infer the subject is a member
-        # @returns hash of URIs
         seeds = {}
         seeds[thisClass.toNT()] = true
         members = {}
@@ -342,6 +365,13 @@
                     members[st.object.toNT()] = st
         return members

+
+    ###
+    transform a collection of NTriple URIs into their URI strings
+    @param t some iterable colletion of NTriple URI strings
+    @return a collection of the URIs as strings
+    todo: explain why it is important to go through NT
+    ###
     NTtoURI: (t) ->
         uris = {}
         for own k, v of t
@@ -354,12 +384,14 @@
     findMemberURIs: (subject) ->
         @NTtoURI @findMembersNT subject

+    ###
+    Get all the Classes of which we can RDFS-infer the subject is a member
+    todo: This will loop is there is a class subclass loop (Sublass loops are not illegal)
+    Returns a hash table where key is NT of type and value is statement why we think so.
+    Does NOT return terms, returns URI strings.
+    We use NT representations in this version because they handle blank nodes.
+    ###
     findTypesNT: (subject) ->
-        # Get all the Classes of which we can RDFS-infer the subject is a member
-        # ** @@ This will loop is there is a class subclass loop (Sublass loops are not illegal)
-        # Returns a hash table where key is NT of type and value is statement why we think so.
-        # Does NOT return terms, returns URI strings.
-        # We use NT representations in this version because they handle blank nodes.
         rdftype = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'
         types = []
         for st in @statementsMatching(subject, undefined, undefined) # fast
@@ -374,27 +406,33 @@
                 types[domain.toNT()] = st
         return @transitiveClosure(types, @sym('http://www.w3.org/2000/01/rdf-schema#subClassOf'), false)

+    ###
+    Get all the Classes of which we can RDFS-infer the subject is a subclass
+    Returns a hash table where key is NT of type and value is statement why we think so.
+    Does NOT return terms, returns URI strings.
+    We use NT representations in this version because they handle blank nodes.
+    ###
     findSuperClassesNT: (subject) ->
-        # Get all the Classes of which we can RDFS-infer the subject is a subclass
-        # Returns a hash table where key is NT of type and value is statement why we think so.
-        # Does NOT return terms, returns URI strings.
-        # We use NT representations in this version because they handle blank nodes.
         types = []
         types[subject.toNT()] = true
         return @transitiveClosure(types, @sym('http://www.w3.org/2000/01/rdf-schema#subClassOf'), false)

+    ###
+    Get all the Classes of which we can RDFS-infer the subject is a superclass
+    Returns a hash table where key is NT of type and value is statement why we think so.
+    Does NOT return terms, returns URI strings.
+    We use NT representations in this version because they handle blank nodes.
+    ###
     findSubClassesNT: (subject) ->
-        # Get all the Classes of which we can RDFS-infer the subject is a superclass
-        # Returns a hash table where key is NT of type and value is statement why we think so.
-        # Does NOT return terms, returns URI strings.
-        # We use NT representations in this version because they handle blank nodes.
         types = []
         types[subject.toNT()] = true
         return @transitiveClosure(types, @sym('http://www.w3.org/2000/01/rdf-schema#subClassOf'), true)

+    ###
+    Find the types in the list which have no *stored* supertypes
+    We exclude the universal class, owl:Things and rdf:Resource, as it is information-free.
+    ###
     topTypeURIs: (types) ->
-        # Find the types in the list which have no *stored* supertypes
-        # We exclude the universal class, owl:Things and rdf:Resource, as it is information-free.
         tops = []
         for own k, v of types
             n = 0
@@ -410,11 +448,13 @@
             delete tops['http://www.w3.org/2002/07/owl#Thing']
         return tops

+    ###
+    Find the types in the list which have no *stored* subtypes
+    These are a set of classes which provide by themselves complete
+    information -- the other classes are redundant for those who
+    know the class DAG.
+    ###
     bottomTypeURIs: (types) ->
-        # Find the types in the list which have no *stored* subtypes
-        # These are a set of classes which provide by themselves complete
-        # information -- the other classes are redundant for those who
-        # know the class DAG.
         bots = []
         for own k, v of types
             subs = @each(undefined, @sym('http://www.w3.org/2000/01/rdf-schema#subClassOf'), @sym(k))


test: Replace `zmq` by `jmp`

test: Replace `zmq` by `jmp`

--- test.coffee
+++ test.coffee
@@ -1,6 +1,7 @@
 fs = require 'fs'
-zmq = require 'zmq'
 _ = require 'lodash'
+jmp = require 'jmp'
+zmq = jmp.zmq

 shell_socket = zmq.socket 'dealer'
 io_socket    = zmq.socket 'sub'
@@ -11,7 +12,7 @@
 shell_socket.on 'message', (msg...) ->
     console.log "new shell message"
     _.forEach(msg,(item) ->
-            console.log "shell received:", item.toString('utf8'))
+        console.log "shell received:", item.toString('utf8'))

 io_socket.on 'message', (msg...) ->
     console.log "new IO message"
@@ -32,26 +33,25 @@

 # console.log io_socket

-header = JSON.stringify({
-            msg_id: 0,
-            username: "will",
-            session: "00000000-0000-0000-0000-000000000000",
-            msg_type: "execute_request",
-            version: "5.0"
-        })
+header = JSON.stringify
+    msg_id: 0,
+    username: "will",
+    session: "00000000-0000-0000-0000-000000000000",
+    msg_type: "execute_request",
+    version: "5.0"

-shell_socket.send(
-    [
-        '<IDS|MSG>',
-        '',
-        header,
-        '{}',
-        '{}',
-        JSON.stringify({
-                code: "a - 4"
-                silent: false
-                store_history: true
-                user_expressions: {}
-                allow_stdin: false
-            })
-    ])
+message = [
+    '<IDS|MSG>',
+    '',
+    header,
+    '{}',
+    '{}',
+    JSON.stringify
+        code: "a - 4"
+        silent: false
+        store_history: true
+        user_expressions: {}
+        allow_stdin: false
+]
+
+shell_socket.send message


Search returns and display results

Search returns and display results

--- app/webroot/coffee/views/search/search.coffee
+++ app/webroot/coffee/views/search/search.coffee
@@ -234,20 +234,20 @@
     e.preventDefault()
     @search.vs.searchBox.addFacet(e.target.text,'',10)

-  search: (e) ->
-    e.preventDefault()
-
-    query = [e.target.text, "like", $(".VS-search-inner").val()]
-    console.log(@search.vs.searchBox)
-    @search.run query,
-      order: 'type'
-      direction: @options.sortDir
+  #USELESS?
+  #search: (e) ->
+  #  e.preventDefault()
+  #
+  #  query = [e.target.text, "like", $(".VS-search-inner").val()]
+  #  console.log(query)
+  #  @search.run query,
+  #    order: 'type'
+  #    direction: @options.sortDir

   # Render the results.
   render: ->
     @_render results: @search.results.toJSON()
     data = @search.results.query
-
     data.page = @search.page
     data.query = encodeURIComponent @search.query
     $('#search-pagination').html arcs.tmpl('search/paginate', results: data)
@@ -257,6 +257,8 @@
   _render: (results, append=false) ->
     $results = $('#search-results')
     template = if @options.grid then 'search/grid' else 'search/list'
-    $results[if append then 'append' else 'html'] arcs.tmpl(template, results)
+    results = @search.results.query.results
+    console.log(@search.results.query.results)
+    $results[if append then 'append' else 'html'] arcs.tmpl(template, results: @search.results.query.results)
     if not @search.results.query.total > 0
       $results.html "
No Results
"


remove static to save memory

remove static to save memory

--- proxy/middleware.coffee
+++ proxy/middleware.coffee
@@ -1,10 +1,12 @@
+log = require './log.coffee'
+
 module.exports =
   reqLogger: (req, res, next) ->
     start = new Date()
     res.on 'end', ->
       end = new Date()
       elapsed = (end - start).toFixed 2
-      console.log "#{start.toString()} #{elapsed}ms #{req.method} #{req.url}"
+      log.info "#{start.toString()} #{elapsed}ms #{req.method} #{req.url}"
     next()

   bodyParser: (req, res, next) ->
@@ -23,20 +25,6 @@
       res.end()
     next()

-  static: (req, res, next) ->
-    files = require './static.coffee'
-    if not (req.url of files)
-      return next()
-    ext = (path) ->
-      ret = path.split '.'
-      ret[ret.length - 1]
-    switch ext req.url
-      when 'html'
-        res.writeHead 200, 'Content-Type': 'text/html'
-      when 'js'
-        res.writeHead 200, 'Content-Type': 'application/javascript'
-    res.end atob files[req.url]
-
   '404': (req, res, next) ->
     if res.headersSent
       return next()


Edit level

Edit level


subscribe to observables in usage-queue-why table

--- js/inner/build.coffee
+++ js/inner/build.coffee
@@ -357,6 +357,14 @@
       @build_channel.bind('updateObservables', @updateObservables)
       @build_channel.bind('maybeAddMessages', @maybeAddMessages)

+  maybeSubscribeObservables: () =>
+    if @shouldSubscribe()
+      @pusher_subscribed(true)
+      @build_channel = VM.pusher.subscribe(@pusherChannel())
+      @build_channel.bind 'pusher:subscription_error', (status) ->
+        _rollbar.push status
+      @build_channel.bind('updateObservables', @updateObservables)
+
   fillActions: (step, index) =>
     # fills up steps and actions such that step and index are valid
     for i in [0..step]
@@ -459,6 +467,7 @@
       complete: () =>
         # stop the spinner if there was an error
         @usage_queue_why([]) if not @usage_queue_why()
+        _.each(@usage_queue_why(), ((b) -> b.maybeSubscribeObservables()))

   toggle_artifacts: () =>
     if @artifacts_visible()


Add GeographicExtent to validation

Add GeographicExtent to validation

--- ui/coffee/edit.coffee
+++ ui/coffee/edit.coffee
@@ -210,8 +210,9 @@
       collections = { Collections: ((JSON.parse JSON.stringify collection).id for collection in @collectionsView.model.models) }
       return _.extend recordJson, basics, geo, authors, distributors, links, collections

-    # This is kinda lame, since validation is being provided by the metadata-server already
-    #   but this makes it easier to track which fields need to be adjusted...
+    # This is very lame, since validation is being provided by the metadata-server already
+    #   but this makes it easier to track which fields need to be adjusted since the
+    #   metadata-server does not return information about WHY validation failed.
     validateRecord: ->
       messages = []
       # Check that required content is filled in
@@ -254,6 +255,25 @@
           descLi.addClass 'invalid'
           messages.push "Descriptions must contain at least 50 characters"

+      # Check that GeographicExtent numbers fall within acceptable bounds
+      $('#geographic-extent-list input').each ->
+        extentLi = $(this).parent 'li'
+        extentLi.removeClass 'invalid'
+        extentVal = $(this).val()
+        switch $(this).attr 'attr'
+          when 'NorthBound'
+            extentLi.addClass 'invalid' if extentVal > 90 or extenVal < -90
+            messages.push 'NorthBound must be between -90 and 90'
+          when 'SouthBound'
+            extentLi.addClass 'invalid' if extentVal > 90 or extenVal < -90
+            messages.push 'SouthBound must be between -90 and 90'
+          when 'EastBound'
+            extentLi.addClass 'invalid' if extentVal > 180 or extenVal < -180
+            messages.push 'EastBound must be between -90 and 90'
+          when 'WestBound'
+            extentLi.addClass 'invalid' if extentVal > 180 or extenVal < -180
+            messages.push 'WestBound must be between -90 and 90'
+
       # List validation messages
       msgContainer = $('#validation-message-container')
       msgContainer.children().each ->


Ace: Update useWordWrap appStorage value when softWrap is not off

--- client/Ace/ace.coffee
+++ client/Ace/ace.coffee
@@ -1,11 +1,3 @@
-###
-  todo:
-
-    - fix setSoftWrap it goes back to off when you reopen the settings
-
-###
-
-
 class Ace extends KDView

   constructor:(options, file)->
@@ -381,7 +373,8 @@

     @editor.getSession().setWrapLimitRange limit, limit
     @editor.renderer.setPrintMarginColumn margin
-    @setUseWordWrap no if value is 'off'
+    wordWrapState = value isnt 'off'
+    @setUseWordWrap wordWrapState, save

     return  unless save
     @appStorage.setValue 'softWrap', value


Fix reactivity in author/@mention list

Fix reactivity in author/@mention list

--- client/message.coffee
+++ client/message.coffee
@@ -1743,7 +1743,9 @@
       Session.set 'threadMentions', threadMentions
       ->
         threadAuthors[author] -= 1 for author in authors
+        Session.set 'threadAuthors', threadAuthors
         threadMentions[author] -= 1 for author in mentions
+        Session.set 'threadMentions', threadMentions
     , [(key for key of message.authors).join('@'), message.title, message.body, usernames]

     ## One-time effects


Inherit unpublished and deleted state from parent (fix #525)

--- client/message.coffee
+++ client/message.coffee
@@ -1362,7 +1362,17 @@
   defaultPublished = useTracker ->
     autopublish()
   , []
-  oncePublished = if defaultPublished then '' else ' (once published)'
+  ## If parent is unpublished or deleted, inherit that state by default
+  ## (in the former case, overriding autopublish setting).
+  defaultPublished and= Boolean message.published
+  defaultDeleted = Boolean message.deleted
+  once = []
+  once.push 'published' unless defaultPublished
+  once.push 'undeleted' if defaultDeleted
+  if once.length
+    once = " (once #{once.join ' and '})"
+  else
+    once = ''
   defaultVariant = if defaultPublished then 'default' else 'warning'
   unless defaultPublished
     prefix ?= 'Unpublished '
@@ -1382,6 +1392,9 @@
         reply.published = false
       when 'true'
         reply.published = true
+      else
+        reply.published = defaultPublished
+    reply.deleted = defaultDeleted
     Meteor.call 'messageNew', message.group, message._id, null, reply, (error, result) ->
       if error
         console.error error
@@ -1402,6 +1415,8 @@
           callbacks[i] = ->
             Meteor.call 'messageNew', message.group, message._id, null,
               file: file2.uniqueIdentifier
+              deleted: defaultDeleted
+              published: defaultPublished
               finished: true
             , done
           ## But call all the callbacks in order by file, so that replies
@@ -1438,7 +1453,7 @@

         else

-            <TextTooltip placement="left" title="Start a new child message of this one, #{if defaultPublished then 'immediately ' else ''}visible to everyone in this thread#{oncePublished}.">
+            <TextTooltip placement="left" title="Start a new child message of this one, #{if defaultPublished then 'immediately ' else ''}visible to everyone in this thread#{once}.">
               <Dropdown.Item href="#" onClick={onReply}>
                 <button className="btn btn-#{defaultVariant} btn-block replyButton">
                   {prefix}
@@ -1451,7 +1466,7 @@
         <>
           {if publicReply

-              <TextTooltip placement="left" title="Start a new child message of this one, visible to everyone in this thread#{oncePublished}.">
+              <TextTooltip placement="left" title="Start a new child message of this one, visible to everyone in this thread#{once}.">
                 <Dropdown.Item href="#" data-privacy="public" onClick={onReply}>
                   <button className="btn btn-#{defaultVariant} btn-block replyButton">
                     {prefix}
@@ -1463,7 +1478,7 @@
           }
           {if privateReply

-              <TextTooltip placement="left" title="Start a new child message of this one, visible only to coauthors and those explicitly given access#{oncePublished}, initially set to coauthors of the message you're replying to.">
+              <TextTooltip placement="left" title="Start a new child message of this one, visible only to coauthors and those explicitly given access#{once}, initially set to coauthors of the message you're replying to.">
                 <Dropdown.Item href="#" data-privacy="private" onClick={onReply}>
                   <button className="btn btn-#{defaultVariant} btn-block replyButton">
                     {prefix}
@@ -1484,24 +1499,29 @@
           </TextTooltip>
         </Dropdown.Item>

-

-        <Dropdown.Item href="#" data-published="#{not defaultPublished}" onClick={onReply}>
-          {if defaultPublished
-            <TextTooltip placement="left" title="Start a new child message of this one that starts in the unpublished state, so it will become generally visible only when you select Action / Publish.">
-              <button className="btn btn-warning btn-block">
-                Unpublished Reply
-              </button>
-            </TextTooltip>
-          else
-            <TextTooltip placement="left" title="Start a new child message of this one that starts in the published state, so everyone in this thread can see it immediately.">
-              <button className="btn btn-success btn-block">
-                {prefix unless prefix == 'Unpublished '}
-                Published Reply
-              </button>
-            </TextTooltip>
-          }
-        </Dropdown.Item>
-

+      {
+      ## Offer second (un)published option only if parent published;
+      ## if parent unpublisehd, then we only offer unpublished above.
+      if message.published
+

+          <Dropdown.Item href="#" data-published="#{not defaultPublished}" onClick={onReply}>
+            {if defaultPublished
+              <TextTooltip placement="left" title="Start a new child message of this one that starts in the unpublished state, so it will become generally visible only when you select Action / Publish.">
+                <button className="btn btn-warning btn-block">
+                  Unpublished Reply
+                </button>
+              </TextTooltip>
+            else
+              <TextTooltip placement="left" title="Start a new child message of this one that starts in the published state, so everyone in this thread can see it immediately#{if defaultDeleted then ' (once undeleted)' else ''}.">
+                <button className="btn btn-success btn-block">
+                  {prefix unless prefix == 'Unpublished '}
+                  Published Reply
+                </button>
+              </TextTooltip>
+            }
+          </Dropdown.Item>
+

+      }
     </Dropdown.Menu>
     <input className="attachInput" type="file" multiple ref={attachInput}
      {...inputProps}/>


added versions

added versions

--- lib/awesomebox.coffee
+++ lib/awesomebox.coffee
@@ -22,14 +22,15 @@
       @versions = new Api.Versions(@client, @app)

     get: (cb) -> @client.get("/apps/#{encode(@app)}", cb)
-    status: (cb) -> @client.get("/apps/#{encode(@app)}/status", cb)
-    stop: (cb) -> @client.get("/apps/#{encode(@app)}/stop", cb)
-    start: (cb) -> @client.get("/apps/#{encode(@app)}/start", cb)
-    logs: (cb) -> @client.get("/apps/#{encode(@app)}/logs", cb)
+    # status: (cb) -> @client.get("/apps/#{encode(@app)}/status", cb)
+    # stop: (cb) -> @client.get("/apps/#{encode(@app)}/stop", cb)
+    # start: (cb) -> @client.get("/apps/#{encode(@app)}/start", cb)
+    # logs: (cb) -> @client.get("/apps/#{encode(@app)}/logs", cb)
     update: (file, cb) ->
       req = @client.put("/apps/#{encode(@app)}", cb)
       req.form().append('file', fs.createReadStream(file))
       req.on('error', cb)
+    version: (version) -> new Api.Version(@client, @app, version)

   Domains: class DomainsApi
     constructor: (@client, @app) ->
@@ -41,6 +42,11 @@
     constructor: (@client, @app) ->
     list: (cb) -> @client.get("/apps/#{encode(@app)}/versions", cb)
     remove: (version, cb) -> @client.delete("/apps/#{encode(@app)}/versions/#{encode(version)}", cb)
+
+  Version: class VersionApi
+    constructor: (@client, @app, @version) ->
+      start: (cb) -> @client.post("/apps/#{encode(@app)}/versions/#{encode(@version)}/start", cb)
+      stop: (cb) -> @client.post("/apps/#{encode(@app)}/versions/#{encode(@version)}/stop", cb)
 }

 class Awesomebox extends Rest


Just reordering some code.

Just reordering some code.

--- client/results.coffee
+++ client/results.coffee
@@ -30,16 +30,16 @@
   if Session.get 'indexActive'
     Meteor.subscribe 'search-available'

+Template.results.created = ->
+  $(window).on 'scroll.results', ->
+    if $(document).height() - $(window).scrollTop() <= 2 * $(window).height()
+      increaseSearchLimit 10
+
 Template.results.rendered = ->
   $(@findAll '.scrubber').iscrubber()

   if Session.get 'currentSearchQueryReady'
     searchLimitIncreasing = false
-
-Template.results.created = ->
-  $(window).on 'scroll.results', ->
-    if $(document).height() - $(window).scrollTop() <= 2 * $(window).height()
-      increaseSearchLimit 10

 Template.results.destroyed = ->
   $(window).off 'scroll.results'


Add descriptions to waitsFor blocks to debug CI issue

--- spec/markdown-preview-spec.coffee
+++ spec/markdown-preview-spec.coffee
@@ -141,7 +141,7 @@
       runs ->
         atom.commands.dispatch workspaceElement, 'markdown-preview:toggle'

-      waitsFor ->
+      waitsFor "initial render", ->
         MarkdownPreviewView::renderMarkdown.callCount > 0

       runs ->
@@ -240,7 +240,7 @@
         waitsForPromise ->
           atom.packages.activatePackage('language-javascript')

-        waitsFor ->
+        waitsFor "render after a new grammar is loaded", ->
           MarkdownPreviewView::renderMarkdown.callCount > 0

   describe "when the markdown preview view is requested by file URI", ->


Call atom.create with TokenizedBuffer before testing it

--- spec/tokenized-buffer-spec.coffee
+++ spec/tokenized-buffer-spec.coffee
@@ -431,7 +431,7 @@
     it "updates the tab length of the tokenized lines", ->
       buffer = atom.project.bufferForPathSync('sample.js')
       buffer.setText('\ttest')
-      tokenizedBuffer = new TokenizedBuffer({buffer})
+      tokenizedBuffer = atom.create(new TokenizedBuffer({buffer}))
       fullyTokenize(tokenizedBuffer)
       expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe '  '
       atom.config.set('editor.tabLength', 6)


Introduce Shape class

Introduce Shape class

--- views/gorillas.coffee
+++ views/gorillas.coffee
@@ -1,3 +1,13 @@
+class Shape
+  constructor:(@context) ->
+
+  draw_circle:(x, y, width, color) ->
+    @context.fillStyle = color
+    @context.beginPath()
+    @context.arc x, y, width, 0, Math.PI*2, true
+    @context.closePath()
+    @context.fill()
+
 class Building
   constructor:(@context, @canvas_height) ->
     @width = 70 + Math.floor(Math.random()*40)
@@ -105,24 +115,20 @@
     1024/2

   draw:() ->
-    @draw_circle()
+    @draw_body()
     @rays()
     @eyes()
     @smile()

-  draw_circle:() ->
-    @context.fillStyle = @color
-    @context.beginPath()
-    @context.arc @position(), @y, @width, 0, Math.PI*2, true
-    @context.closePath()
-    @context.fill()
+  draw_body:() ->
+    shape = new Shape(@context)
+    shape.draw_circle(@position(), @y, @width, @color)

   eyes:() ->
-    @context.fillStyle = '#000000'
-    @context.beginPath()
-    @context.arc @position()-10, @y-10, 5, 0, Math.PI*2,  true
-    @context.arc @position()+10, @y-10, 5, 0, Math.PI*2,  true
-    @context.fill()
+    shape = new Shape(@context)
+    color = '#000000'
+    shape.draw_circle(@position()-10, @y-10, 5, color)
+    shape.draw_circle(@position()+10, @y-10, 5, color)

   smile:() ->
     @context.strokeStyle = '#000000'


deleted debug line

deleted debug line

--- coffee/_player.coffee
+++ coffee/_player.coffee
@@ -123,17 +123,13 @@
     if e.keyCode is 37 and e.target.tagName != 'INPUT'
         PlayPrevious(__currentTrack.artist, __currentTrack.title)
     if e.keyCode is 39 and e.target.tagName != 'INPUT'
-    #    PlayNext(__currentTrack.artist, __currentTrack.title)
-     win.showDevTools()
-
+        PlayNext(__currentTrack.artist, __currentTrack.title)

 $('#player-container #info #track-info #action i').click ->
     if $(@).hasClass('play')
         videojs('video_player').play()
     else
         videojs('video_player').pause()
-
-

 videojs('video_player').ready ->
     @.on 'loadedmetadata', ->


change gravity

change gravity

--- views/gorillas.coffee
+++ views/gorillas.coffee
@@ -141,7 +141,7 @@
     @inity = initial_y
     @x = 0
     @y = 0
-    @g = -9.8
+    @g = -9.80665

     F = 40
     angle = 70


Fix a scope issue :(

Fix a scope issue :(

--- coffee/rungame.coffee
+++ coffee/rungame.coffee
@@ -217,15 +217,16 @@
 	$("#answers").empty()
 	for i in [0...diagram.length]
 		li = $("

")
+		console.log(li)
+		do (i, li) -> # wef JS scoping can go die
+			li.click () ->
+				diagram.markAnswer(i)
+				enableButtons()
+				markAllActive()
+				updateSidebarSoft()
 		li.addClass("answer")
-		li.attr("data-index", i)
 		li.html(diagram.getAnswer(i).join(" "))
 		$("#answers").append(li)
-		li.click () ->
-			diagram.markAnswer(li.attr("data-index"))
-			enableButtons()
-			markAllActive()
-			updateSidebarSoft()

 # }}}
 # UI Triggers {{{


Refactored the Exo.List template rendering function to be more readable.

--- list.coffee
+++ list.coffee
@@ -33,9 +33,8 @@
 			@renderControllers collection

 	renderTemplates: (collection) ->
-		templates = @templates || {default: @template}
-		@html collection.map (item) => templates[item.getClassName()] or templates.default
-			.call(@, [item])[0]
+		templates = @templates || {default: @template}
+		@html collection.map (item) => (templates.default or templates[item.getClassName()]).call(@, [item])[0]

 	renderControllers: (collection) ->
 		controllers = @controllers || {default: @controller}


Include original XML node in presence and message notifications.

--- web/lib/coffeescripts/session.coffee
+++ web/lib/coffeescripts/session.coffee
@@ -201,6 +201,7 @@
         text: body.text()
         html: html.text()
         received: new Date()
+        node: node
     true # keep handler alive

   handlePresence: (node) ->
@@ -219,6 +220,7 @@
       offline: type == 'unavailable' || type == 'error'
       away:    show.text() == 'away' || show.text() == 'xa'
       dnd:     show.text() == 'dnd'
+      node:    node
     contact = @roster[from.split('/')[0]]
     contact.update presence if contact
     this.notify 'presence', presence


Delete main.coffee

Delete main.coffee

--- main.coffee
+++ main.coffee
@@ -1 +0,0 @@
-console.log 'test'


Some kind of SQL profiler.

Some kind of SQL profiler.

--- main.coffee
+++ main.coffee
@@ -17,8 +17,45 @@
 'use strict'

 config = require './config.js'
+
 anyDB = require 'any-db'
 dbConnection = anyDB.createPool config.DATABASE_URL, min: 2, max: 20
+dbConnection._query = dbConnection.query
+dbConnection.query = (q, data, cb) ->
+	logged = q
+	for value, index in data
+		index++
+		while logged.indexOf("$#{index}") isnt -1
+			logged = logged.replace "$#{index}", "<#{value}>"
+	Date start = new Date()
+	@_query q, data, (error, result) ->
+		time = new Date() - start
+		console.log "\n#{time} ms: #{logged}\n"
+		cb(error, result)
+dbConnection._begin = dbConnection.begin
+dbConnection.begin = ->
+	console.log "\nBEGIN TRANSACTION\n"
+	tx = @_begin()
+	tx.____start = new Date()
+	tx.__query = tx.query
+	tx.query = (q, data, cb) ->
+		logged = q
+		for value, index in data
+			index++
+			while logged.indexOf("$#{index}") isnt -1
+				logged = logged.replace "$#{index}", "<#{value}>"
+		Date start = new Date()
+		@__query q, data, (error, result) ->
+			time = new Date() - start
+			console.log "\n#{time} ms: t: #{logged}\n"
+			cb(error, result)
+	# doesn't work
+	#tx.__commit = tx.commit
+	#tx.commit = (a) ->
+		#console.log "\nCOMMIT, the whole thing took #{new Date() - tx.____start} ms\n"
+		#tx.__commit a
+	return tx
+
 lib = require './lib.coffee'
 async = require 'async'
 express = require 'express'


:fishing_pole_and_fish::grin: Updated at https://danielx.net/editor/

--- main.coffee
+++ main.coffee
@@ -164,7 +164,7 @@
 require("./midi_access")().handle ({data}) ->
   event = MidiFile.readEvent Stream(data), true

-  player?.handleEvent event, time: 0
+  player?.handleEvent event, time: context.currentTime - timeOffset

 -> #TODO Offline rendering
   offlineContext = new OfflineAudioContext(2, 44100*40, 44100)


test `_paypal.checAccountExists`.

test `_paypal.checAccountExists`.

--- services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee
+++ services/web/test/UnitTests/coffee/Subscription/RecurlyWrapperTests.coffee
@@ -655,3 +655,103 @@
 					@setAddress.callCount.should.equal 0
 					@createSubscription.callCount.should.equal 0
 					done()
+
+	describe '_paypal.checkAccountExists', ->
+
+		beforeEach ->
+			@apiRequest = sinon.stub(@RecurlyWrapper, 'apiRequest')
+			@_parseAccountXml = sinon.spy(@RecurlyWrapper, '_parseAccountXml')
+			@cache =
+				user: @user = {_id: 'some_id'}
+				recurly_token_id: @recurly_token_id = "some_token"
+				subscriptionDetails: @subscriptionDetails =
+					currencyCode: "EUR"
+					plan_code:    "some_plan_code"
+					coupon_code:  ""
+					isPaypal: true
+					address:
+						address1: "addr_one"
+						address2: "addr_two"
+						country:  "some_country"
+						state:    "some_state"
+						zip:      "some_zip"
+			@call = (callback) =>
+				@RecurlyWrapper._paypal.checkAccountExists @cache, callback
+
+		afterEach ->
+			@apiRequest.restore()
+			@_parseAccountXml.restore()
+
+		describe 'when the account exists', ->
+
+			beforeEach ->
+				resultXml = '<account><account_code>abc</account_code></account>'
+				@apiRequest.callsArgWith(1, null, {statusCode: 200}, resultXml)
+
+			it 'should not produce an error', (done) ->
+				@call (err, result) =>
+					expect(err).to.not.be.instanceof Error
+					done()
+
+			it 'should call apiRequest', (done) ->
+				@call (err, result) =>
+					@apiRequest.callCount.should.equal 1
+					done()
+
+			it 'should call _parseAccountXml', (done) ->
+				@call (err, result) =>
+					@RecurlyWrapper._parseAccountXml.callCount.should.equal 1
+					done()
+
+			it 'should add the account to the cumulative result', (done) ->
+				@call (err, result) =>
+					expect(result.account).to.not.equal null
+					expect(result.account).to.not.equal undefined
+					expect(result.account).to.deep.equal {
+						account_code: 'abc'
+					}
+					done()
+
+		describe 'when the account does not exist', ->
+
+			beforeEach ->
+				@apiRequest.callsArgWith(1, new Error('not found'), {statusCode: 404}, '')
+
+			it 'should not produce an error', (done) ->
+				@call (err, result) =>
+					expect(err).to.not.be.instanceof Error
+					done()
+
+			it 'should call apiRequest', (done) ->
+				@call (err, result) =>
+					@apiRequest.callCount.should.equal 1
+					done()
+
+			it 'should not call _parseAccountXml', (done) ->
+				@call (err, result) =>
+					@RecurlyWrapper._parseAccountXml.callCount.should.equal 0
+					done()
+
+			it 'should not add the account to result', (done) ->
+				@call (err, result) =>
+					expect(result.account).to.equal undefined
+					done()
+
+	# describe '_paypal.createAccount', ->
+
+	# 	beforeEach ->
+
+
+	# describe '_paypal.createBillingInfo', ->
+
+	# 	beforeEach ->
+
+
+	# describe '_paypal.setAddress', ->
+
+	# 	beforeEach ->
+
+
+	# describe '_paypal.createSubscription', ->
+
+	# 	beforeEach ->


added progress to replace image on load progress

--- dev/coffee/script.coffee
+++ dev/coffee/script.coffee
@@ -135,12 +135,38 @@
         Rusnet.show_all()
       return

-    $('img').on 'click', (event) ->
+    $('#top').on 'click', (event) ->
       event = event or window.event
       target = event.target or event.srcElement
+
       if settings.images.index < (settings.images.count - 1) then settings.images.index += 1 else settings.images.index = 0
-      img = settings.images.list[settings.images.index]
-      $(target).attr 'src', ["/img/", img].join("")
+
+      url = settings.images.list[settings.images.index]
+
+      $('#top').html """
+

+

+
+
+
+
+
+
+
+
+
+
+

+

+      """
+
+      img_w = new Image()
+      $(img_w).on 'load', (event) ->
+        $('#top').html img_w
+        return
+
+      img_w.src = ['/img/', url].join("")
+      return
     return

   @show_all = ->
@@ -183,6 +209,7 @@
       url: '/json/config.json'
       type: 'get'
       dataType: 'json'
+      async: false
       success: (data) ->
         if data.images
           length = data.images.length
@@ -194,7 +221,6 @@
           $('img').removeClass 'hide'

       error: (err) ->
-        console.log "err:", err
         return
     return


Avoid exception when the dongle is unplugged

--- app/src/app.coffee
+++ app/src/app.coffee
@@ -58,7 +58,7 @@
     onDongleConnected: (dongle) ->
       if @isInWalletMode() and not dongle.isInBootloaderMode()
         @performDongleAttestation()
-        ledger.tasks.TickerTask.instance.start()
+        ledger.tasks.TickerTask.instance.startIfNeccessary()

     onDongleCertificationDone: (dongle, error) ->
       return unless @isInWalletMode()


Add log.error snippet

Add log.error snippet

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -486,6 +486,10 @@
     'prefix': 'logdebug'
     'body': 'log.debug({title: \'${1:title}\', details: ${2:details}});'

+  'log.error':
+    'prefix': 'logerror'
+    'body': 'log.error({title: \'${1:title}\', details: ${2:details}});'
+
   'search.create':
     'prefix': 'searchcreate'
     'body': 'search.create({\n\ttype: \'${1:type}\'$2\n})$3'


Make NOT NULL snippet an inline constraint

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -700,7 +700,7 @@

   'not null':
     'prefix': 'notnull'
-    'body': 'NOT NULL(${1:column})$2'
+    'body': 'NOT NULL'

   'unique':
     'prefix': 'unique'


Shorten moduleexports snippet to modexp

Shorten moduleexports snippet to modexp

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -238,7 +238,7 @@
     'body': 'require(\'${1:mname}\');$4'

   'module.exports':
-    'prefix': 'moduleexports'
+    'prefix': 'modexp'
     'body': 'module.exports = ${1:object};'

   # Mocha


die on hit obstacle, fade out, reset

die on hit obstacle, fade out, reset

--- obot.coffee
+++ obot.coffee
@@ -34,6 +34,10 @@

 ###

+###
+How does death work?
+just reset after animation
+###

 level_cnv = document.getElementById('level')

@@ -109,7 +113,12 @@
     o.render(ctx, show_frame, frame_t, t)

   # bot
-  @bot.render(ctx)
+  @bot.render(ctx,
+    if @animation_mode == levelAnimMode.DYING
+      (t - @move_control.start.t) * bot_speed
+    else
+      0
+  )

   # commands
   ctx = prog_ctx
@@ -147,7 +156,7 @@
   (x: -bot_size * (hr3-rr3), y: bot_size/2)
 ]

-renderBot = (ctx) ->
+renderBot = (ctx, dying) ->
   bp = bot_points
   ctx.save()
   ctx.translate((@showxi + .5) * cell_size, (@showyi + .5) * cell_size)
@@ -160,8 +169,13 @@
   ctx.closePath()

   ctx.lineWidth = 1.5
-  ctx.strokeStyle = 'white'
   ctx.fillStyle = 'black'
+
+  intensity = Math.floor((1-dying)*(1-dying)*255)
+  if dying == 0
+    ctx.strokeStyle = 'white'
+  else
+    ctx.strokeStyle = "rgb(#{intensity},#{intensity},#{intensity})"

   ctx.fill()
   ctx.stroke()
@@ -453,8 +467,10 @@
   level.commands = level.commands[...-1]

   level.resetAndRun()
-  while level.next_command < level.commands.length && level.step()
-    true
+  while level.next_command < level.commands.length
+    if !level.step()
+      level.reset()
+      break

   level.mode = levelMode.STOP

@@ -536,6 +552,7 @@
 levelAnimMode =
   READY: 1  # ready to run the next command
   MOVING: 2 # animating movement
+  DYING: 3  # animating move into an invalid state

 resetLevel = ->
   @bot.showxi = @bot.xi = @initial_bot_location.xi
@@ -615,17 +632,25 @@
         @animation_mode = levelAnimMode.MOVING
         @show_current_command = step_command
       else
-        # TODO: Need to play a death animation and restart
+        @move_control = next_move_control
+        @animation_mode = levelAnimMode.DYING
+        @show_current_command = step_command

   # immediately run this if we switched to MOVING above
-  if @animation_mode == levelAnimMode.MOVING
+  if @animation_mode == levelAnimMode.MOVING or
+     @animation_mode == levelAnimMode.DYING
     rel_t = (t-@move_control.start.t)/@move_control.duration
-    @bot.showxi = lerp rel_t, @move_control.start.x, @bot.xi
-    @bot.showyi = lerp rel_t, @move_control.start.y, @bot.yi
-    @bot.showdir = @bot.dir

     if rel_t >= 1
-      @animation_mode = levelAnimMode.READY
+      if @animation_mode == levelAnimMode.DYING
+        # revert after death
+        @reset()
+      else # MOVING
+        @animation_mode = levelAnimMode.READY
+    else
+      @bot.showxi = lerp rel_t, @move_control.start.x, @bot.xi
+      @bot.showyi = lerp rel_t, @move_control.start.y, @bot.yi
+      @bot.showdir = @bot.dir

   @last_t = t

@@ -645,8 +670,7 @@
   for o in @obstacles
     if o.isHit(@bot.xi, @bot.yi, @frame)
       console.log("hit obstacle at frame " + @frame)
-      # TODO: error display, take back move (I think that goes elsewhere)
-      #return false
+      return false

   @advanceNextCommand()


Fix boot bug where brain hasn't loaded yet, now deferred

--- src/ical-topic.coffee
+++ src/ical-topic.coffee
@@ -100,4 +100,5 @@
     for room of calendars
       updateTopicForRoom room

-  new cronJob(cronTime, updateTopics, null, true)
+  robot.brain.once 'loaded', ->
+    new cronJob(cronTime, updateTopics, null, true)


MachineSettingsModal: Use anonymous functions for promise callbacks

When we use them with @bound they are attaching themselves into promise chain
and it causes lots of problem.

--- client/Main/machinesettingsmodal.coffee
+++ client/Main/machinesettingsmodal.coffee
@@ -113,7 +113,7 @@
         @showLoader()
         KD.singletons.computeController.build machine

-    computeController.on "public-#{machine._id}", @bound 'updateState'
+    computeController.on "public-#{machine._id}", (event)=> @updateState event

     @over.hide()  if machine.status.state not in [
       Terminated, NotInitialized, Building, Terminating


referral: track share link click

referral: track share link click

--- client/app/CommonViews/sharelink.coffee
+++ client/app/CommonViews/sharelink.coffee
@@ -15,6 +15,8 @@
       "#{provider}-share-dialog",
       "width=626,height=436,left=#{Math.floor (screen.width/2) - (500/2)},top=#{Math.floor (screen.height/2) - (350/2)}"
     )
+
+    KD.kdMixpanel.track "#{provider} Share Link Clicked", $user: KD.nick()

 class TwitterShareLink extends ShareLink
   constructor: (options = {}, data) ->


Editor: Jump to first line when a new file is opened.

--- client/ide/views/tabview/ideview.coffee
+++ client/ide/views/tabview/ideview.coffee
@@ -68,6 +68,9 @@

       ace.on 'FindAndReplaceViewRequested', (withReplaceMode) ->
         appManager.tell 'IDE', 'showFindReplaceView', withReplaceMode
+
+      ace.editor.scrollToRow 0
+      editorPane.goToLine 1

       callback editorPane


Tests: Disable Emoji tests

Tests: Disable Emoji tests

--- client/test/lib/teams/teamspostwith.coffee
+++ client/test/lib/teams/teamspostwith.coffee
@@ -55,11 +55,11 @@
     browser.end()

-  postMessageWithEmoji: (browser) ->
+  # postMessageWithEmoji: (browser) ->

-    messageWithEmoji = ':smiley: :+1:'
+  #   messageWithEmoji = ':smiley: :+1:'

-    user = teamsHelpers.loginTeam(browser)
-    teamsHelpers.createChannel(browser, user)
-    teamsHelpers.sendComment(browser, messageWithEmoji, 'messageWithEmoji')
-    browser.end()
+  #   user = teamsHelpers.loginTeam(browser)
+  #   teamsHelpers.createChannel(browser, user)
+  #   teamsHelpers.sendComment(browser, messageWithEmoji, 'messageWithEmoji')
+  #   browser.end()


Fixed a syntax bug

Fixed a syntax bug

--- src/Quaternion.coffee
+++ src/Quaternion.coffee
@@ -45,25 +45,26 @@
     else
       k = if matYAxis.y > matXAxis.x then (if matZAxis.z > matYAxis.y then 2 else 1) else (if matZAxis.z > matXAxis.x then 2 else 0)

-      if k is 0
-        root = Math.sqrt matXAxis.x - (matYAxis.y + matZAxis.z) + 1
-        scale = if root isnt 0 then 0.5 / root else root
-        @set root * 0.5, (matXAxis.y + matYAxis.x) * scale, (matZAxis.x + matXAxis.z) * scale, (matYAxis.z - matZAxis.y) * scale
-      else if k is 1
-        root = Math.sqrt matYAxis.y - (matZAxis.z + matXAxis.x) + 1
-        scale = if root isnt 0 then 0.5 / root else root
-        @set (matXAxis.y + matYAxis.x) * scale, root * 0.5, (matYAxis.z + matZAxis.y) * scale, (matZAxis.x - matXAxis.z) * scale
-      else # k is 2
-        root = Math.sqrt matZAxis.z - (matXAxis.x + matYAxis.y) + 1
-        scale = if root isnt 0 then 0.5 / root else root
-        @set (matZAxis.x + matXAxis.z) * scale, (matYAxis.z + matZAxis.y) * scale, root * 0.5, (matXAxis.y - matYAxis.x) * scale
+      switch k
+        when 0
+          root = Math.sqrt matXAxis.x - (matYAxis.y + matZAxis.z) + 1
+          scale = if root isnt 0 then 0.5 / root else root
+          @set root * 0.5, (matXAxis.y + matYAxis.x) * scale, (matZAxis.x + matXAxis.z) * scale, (matYAxis.z - matZAxis.y) * scale
+        when 1
+          root = Math.sqrt matYAxis.y - (matZAxis.z + matXAxis.x) + 1
+          scale = if root isnt 0 then 0.5 / root else root
+          @set (matXAxis.y + matYAxis.x) * scale, root * 0.5, (matYAxis.z + matZAxis.y) * scale, (matZAxis.x - matXAxis.z) * scale
+        else # 2
+          root = Math.sqrt matZAxis.z - (matXAxis.x + matYAxis.y) + 1
+          scale = if root isnt 0 then 0.5 / root else root
+          @set (matZAxis.x + matXAxis.z) * scale, (matYAxis.z + matZAxis.y) * scale, root * 0.5, (matXAxis.y - matYAxis.x) * scale

     return @

   slerp: (to, ratio) ->
     if ratio > 1 - DivSugar.EPSILON
       @set to
-    else if  ratio >= DivSugar.EPSILON
+    else if ratio >= DivSugar.EPSILON
       quat = DivSugar.Quaternion._tmpQuat1
       cosOmega = @x * to.x + @y * to.y + @z * to.z + @w * to.w

@@ -76,7 +77,7 @@
       if cosOmega >= 1
         @set to
       else
-        omega = Math.acos if cosOmega > 1 else 1 : cosOmega
+        omega = Math.acos if cosOmega > 1 then 1 else cosOmega
         sinOmega = Math.sin omega
         scale0 = Math.sin(omega * (1 - ratio)) / sinOmega
         scale1 = Math.sin(omega * ratio) / sinOmega


fix a regression so KDSelectBox values end up in the aggregated form data

--- client/Framework/components/inputs/KDSelectBox.coffee
+++ client/Framework/components/inputs/KDSelectBox.coffee
@@ -7,7 +7,7 @@
     super options

   setDomElement:(cssClass)->
-    @inputName  = @getOption name
+    @inputName  = @getOption "name"
     name        = "name='#{@options.name}'"
     @domElement = $ """
         <div class='kdselectbox #{cssClass}'>


activity: set `SocialApiController` singleton instance as channel to public feed

i'm not so happy about it

--- client/Social/Activity/sidebar/activitysidebar.coffee
+++ client/Social/Activity/sidebar/activitysidebar.coffee
@@ -48,6 +48,7 @@
     ,
       name     : 'koding_public_feed'
       id       : "#{KD.getGroup().socialApiChannelId ? 1}"
+      channel  : KD.singleton "socialapi"

     publicLink.addSubView @publicFeedCount = new KDCustomHTMLView
       cssClass : 'count'


Activity: Views: ActivityList Item button title is changed DONE to UPGRADE

--- client/activity/lib/views/activitylistitemview.coffee
+++ client/activity/lib/views/activitylistitemview.coffee
@@ -107,7 +107,7 @@

     @editWidgetWrapper.addSubView new KDButtonView
       style     : 'solid green mini fr done-button'
-      title     : 'DONE'
+      title     : 'UPDATE'
       callback  : =>
         @editWidget.submit @editWidget.input.getValue()


sidebarsection: add try icon to handle open Stack Editor

--- client/app/lib/components/sidebarsection/index.coffee
+++ client/app/lib/components/sidebarsection/index.coffee
@@ -18,9 +18,19 @@

   renderUnreadCount: ->
-    <UnreadCount
-      count={@props.unreadCount}
-      onClick={@bound 'handleUnreadCountClick'} />
+
+    unless @props.originalTemplateUpdate
+      <UnreadCount
+        count={@props.unreadCount}
+        onClick={@bound 'handleUnreadCountClick'} />
+    else
+      <UnreadCount
+        count={1}
+        onClick={@bound 'handleOpenStackEditor'} />
+
+
+  handleOpenStackEditor: ->
+    kd.singletons.router.handleRoute "/Stack-Editor/#{@props.baseStackId}"

   handleUnreadCountClick: ->


Pass onBlur onto field for revealing errors

--- client/lanes/components/select-field/SelectField.cjsx
+++ client/lanes/components/select-field/SelectField.cjsx
@@ -111,6 +111,7 @@
             textField="label"
             onChange={@onChange}
             name={@props.name}
+            onBlur={@onFieldInteraction}
             {...props}
             value={@getCurrentSelection()}
             />
@@ -119,6 +120,10 @@
         if @props.unstyled
             select
         else
-            <LC.FormGroup {...@props} className={@formGroupClassNames()} label={label}>
+            <LC.FormGroup
+                {...@props}
+                className={@formGroupClassNames()}
+                label={@getLabelValue()}
+            >
                 {select}
             </LC.FormGroup>


Fixes tracking and API normalization

Fixes tracking and API normalization

--- src/client/api.coffee
+++ src/client/api.coffee
@@ -251,7 +251,7 @@
       Model = BaseHullModel.extend
         url: ->
           if (@id || @_id)
-            url = normalizeAPIArguments([@_id || @id])[0]
+            url = @_id || @id
           else
             url = @collection?.url
           url
@@ -302,7 +302,7 @@

       core.data.api = api
       core.track = (eventName, params)->
-        api("track/#{eventName}", 'post', params)
+        api({provider:"track", path: eventName}, 'post', params)

       sandbox.track = (eventName, params)->
         core.track(eventName, params)


Remove stale check

Remove stale check

--- src/cmd/remove.coffee
+++ src/cmd/remove.coffee
@@ -35,7 +35,7 @@
   for r in locals.list() when semver.match r.id, filter
     remove r
     return
-  throw Error 'Specified Node.js version not installed!' unless x
+  throw Error 'Specified Node.js version not installed!'

 remove = (x)->
   echo "removing #{x.path}"


Update collection.coffee

Update collection.coffee

@rogierslag What about this?

--- src/collection.coffee
+++ src/collection.coffee
@@ -8,7 +8,7 @@
     @model  = options.model  or @model or Tails.Model
     @parent = options.parent or @parent
     @synced = options.synced or false
-    @alternativeUrl = options.url or null
+    @url    = options.url    or @url

     @on 'change', ( model ) =>
       @synced = false
@@ -23,10 +23,6 @@
     return inflection.transform(@model.name or @model.toString().match(/^function\s*([^\s(]+)/)[1], ['underscore', 'pluralize'])

   url: ( ) ->
-    # If the collection overrides the URL, use that URL
-    if @alternativeUrl?
-      return @alternativeUrl
-
     base = @parent?.url?() or @parent?.url or Tails.config.url
     root = @urlRoot?() or @urlRoot
     format = if @format? then '.' + (@format?() or @format) else ''


Support stores, add setStore method.

Support stores, add setStore method.

--- src/crowdstart.coffee
+++ src/crowdstart.coffee
@@ -7,9 +7,12 @@
   setKey: (key) ->
     @key = key

+  setStore: (id) ->
+    @storeId = id
+
   req: (uri, data, cb) ->
     xhr
-      uri:  (@endpoint.replace /\/$/, '') + uri
+      uri: (@endpoint.replace /\/$/, '') + uri
       method: 'POST'
       headers:
         'Content-Type': 'application/json'
@@ -19,9 +22,19 @@
       cb res.statusCode, body, res.headers.location

   authorize: (data, cb) ->
+    uri = '/authorize'
+
+    if @storeId?
+      uri = "/store/#{@storeId}" + uri
+
     @req '/authorize', data, cb

   charge: (data, cb) ->
+    uri = '/charge'
+
+    if @storeId?
+      uri = "/store/#{@storeId}" + uri
+
     @req '/charge', data, cb

 module.exports = Crowdstart


deal with a few more cases, and switch off

--- src/json_checker.iced
+++ src/json_checker.iced
@@ -1,30 +1,28 @@
-
 #--------------------------------------------------

-parse = (key) ->
+parse_key = (key) ->
   # First split into dict keys
   tmp = ({ type : 'dict', key : k } for k in key.split /\./)

-  split_array_indices = (out, item) ->
-    console.log item.key
-    if (m = item.key.match /^([^[]+)((?:\[\d+\])+)$/)
-      console.log m
-      item.key = m[1]
-      out.push item
+  out = []
+
+  # Now deal with array indices, like 'foo[3][50][1]'
+  for item in tmp
+    if (m = item.key.match /^([^[]*)((?:\[\d+\])+)$/)
+      if m[1]? and m[1].length
+        item.key = m[1]
+        out.push item
       indices = m[2][1...-1].split /\]\[/
       for index in indices
         out.push { type : 'array', key : parseInt(index, 10) }
     else
       out.push item

-  out = []
-  for item in tmp
-    split_array_indices out, item
   return out

 #--------------------------------------------------

 exports.json_checker = json_checker = ({key, template, json}) -> {}

-console.log parse "shit.bar[4][5].bizzle[50].shit"
+console.log parse_key "[3][4][5].shit.bar[4][5].bizzle[50].shit"


redefine wall kind as log

redefine wall kind as log

--- src/load/kinds.coffee
+++ src/load/kinds.coffee
@@ -25,14 +25,14 @@
         "M10,#{SCALE} a3,2 0 1,0 42,0 a3,2 0 1,0 -42,0z"
       ]
     }
-    wall: {
-      name:
+    log: {
+      name: 'log'
       editable: false
       obstruction: true
       position: [7,0]
       fill: 'brown'
       paths: [
-        "M#{OFFSET},#{OFFSET} h#{INSET} v#{INSET} h-#{INSET} v-#{INSET}"
+        "M#{OFFSET+ INSET/2},#{OFFSET} h#{INSET/2} v#{INSET} h-#{INSET/2} v-#{INSET}"
       ]
     }
     egg: {


Update projects listing with new file-icons folder

--- projects.cson
+++ projects.cson
@@ -28,8 +28,7 @@
 		title: "file-icons"
 		paths: [
 			"~/Labs/file-icons/atom"
-			"~/Labs/file-icons/fs"
-			"~/Labs/file-icons/source"
+			"~/Labs/file-icons/icons"
 		]
 		icon: "atom-icon"
 		devMode: true


More predefined

More predefined

--- src/predefined.coffee
+++ src/predefined.coffee
@@ -209,8 +209,8 @@
 #        [ ] Solr
 #        [ ] Sphinx
 #        [ ] Swish
-#    [ ] Server Specific Extensions
-#        [ ] Apache
+#    [/] Server Specific Extensions
+#        [x] Apache
 #        [ ] FastCGI Process Manager
 #        [ ] IIS
 #        [ ] NSAPI
@@ -3475,4 +3475,21 @@
     'pg_untrace': 'function'
     'pg_update': 'function'
     'pg_version': 'function'
+
+    ### Server Specific Extensions ###
+
+    # Apache
+    # Apache functions
+    'apache_child_terminate': 'function'
+    'apache_get_modules': 'function'
+    'apache_get_version': 'function'
+    'apache_getenv': 'function'
+    'apache_lookup_uri': 'function'
+    'apache_note': 'function'
+    'apache_request_headers': 'function'
+    'apache_reset_timeout': 'function'
+    'apache_response_headers': 'function'
+    'apache_setenv': 'function'
+    'getallheaders': 'function'
+    'virtual': 'function'
 }


use the main-bundle layout if bundle exists at pkgRoot

--- src/server/app.coffee
+++ src/server/app.coffee
@@ -6,6 +6,7 @@
 express = require('express')
 ehbs = require("express3-handlebars")
 PTYServer = require "./pty-server"
+fs = require 'fs'
 path = require 'path'

 pkgRoot = path.normalize(path.join __dirname, "../..")
@@ -23,8 +24,17 @@

     @express.set("root",@contentRoot)

+    # If pkgRoot/bundle exists, then use the bundled client assets.
+    # Use a layout template that uses the client assets. Else use the
+    # client assets in build.
+    # FIXME hmmm... should probably use a --dev flag instead
+    if fs.existsSync(path.join(pkgRoot,"bundle"))
+      defaultLayout = "main-bundle"
+    else
+      defaultLayout = "main"
+
     hbsViewEngine = ehbs
-      defaultLayout: 'main'
+      defaultLayout: defaultLayout
       extname: '.hbs.html'
       layoutsDir: "#{serverDir}/views/layouts"
       partialsDir: "#{serverDir}/views/partials"


change class name

change class name


Ensured changing a value to empty adds it to the changed values map.

--- src/Forms.coffee
+++ src/Forms.coffee
@@ -46,13 +46,13 @@
           # TODO(aramk) Sometimes form fields are skipped when retrieving their values.
           if formArgs.submitDiff
             # Remove fields in the modifiers which haven't been changed.
-            $input = $(@autoSaveChangedElement)
+            # $input = $(@autoSaveChangedElement)
             changes = Form.getDocChanges(@template)
             _.each ['$set', '$unset'], (propName) ->
               fields = modifier[propName]
               if fields?
                 _.each fields, (value, key) ->
-                  unless changes[key]? then delete fields[key]
+                  if changes[key] == undefined then delete fields[key]
           modifier

       onError: (operation, error) ->
@@ -415,8 +415,8 @@
           value = Forms.getInputValue($input)
           value = parseFloat(value).toFixed(2)
           Forms.setInputValue($input, value)
-      template.origFormDoc = Form.getInputValues(template)
-
+      template.formDoc = Form.getInputValues(template)
+
     Form.getFormTitle = ->
       collectionName = Collections.getTitle(Form.getCollection())
       singularName = Form.getSingularName()
@@ -456,16 +456,17 @@
     Form.getDocChanges = (template) ->
       template = getTemplate(template)
       formDoc = Form.getInputValues(template)
-      origFormDoc = template.origFormDoc ? {}
+      prevFormDoc = template.formDoc ? {}
       # Form doc is the full set of fields which the doc supports. We must avoid creating modifiers
       # containing any other fields, since this form does not affect them.
       keys = _.keys(formDoc)
       changes = {}
       _.each keys, (key) ->
         formValue = formDoc[key]?.toString().trim() ? ''
-        origValue = origFormDoc[key]?.toString().trim() ? ''
-        if formValue != '' && formValue != origValue
-          changes[key] = formValue
+        prevValue = prevFormDoc[key]?.toString().trim() ? ''
+        if formValue != prevValue
+          # Ensure empty values are null.
+          changes[key] = formValue || null
       changes

     # Merges the latest document into the form, giving precedence to the changed values in the form.


Fix permissions selector for forks.

Fix permissions selector for forks.

--- app/assets/javascripts/repository/edit_remote.js.coffee
+++ app/assets/javascripts/repository/edit_remote.js.coffee
@@ -12,6 +12,9 @@

     input_access_el    = $('#repository_access')[0]
     source_address_el  = $('#repository_source_address')
+    remote_type_els    = $('input[name="repository[remote_type]"]')
+    remote_type_mirror_el = $('#repository_remote_type_mirror')
+
     options_non_mirror = _.clone($('#access_options_non_mirror')[0].options)
     options_mirror     = _.clone($('#access_options_mirror')[0].options)

@@ -31,20 +34,28 @@
       else
         set_options(options_non_mirror, to_non_mirror_option, last_value)

+    is_mirror_selected = () ->
+      remote_type_mirror_el.is(':checked')
+
     is_source_address_set = () ->
       source_address_el.val().trim().length > 0

+    was_mirror_selected = is_mirror_selected()
+
     was_source_address_set = is_source_address_set()
+
+    reset_mirror_selected = () ->
+      was_mirror_selected = is_mirror_selected()

     reset_was_source_address_set = () ->
       was_source_address_set = is_source_address_set()

     change_options = (event) ->
-      if !was_source_address_set && is_source_address_set()
+      if !was_mirror_selected && is_mirror_selected()
         modify_access_list 'mirror'
-      else if was_source_address_set && !is_source_address_set()
+      else if was_mirror_selected && !is_mirror_selected()
         modify_access_list 'non_mirror'
-      else if !was_source_address_set && !is_source_address_set()
+      else if !was_mirror_selected && !is_mirror_selected()
         modify_access_list 'non_mirror'

     change_type_display = (event) ->
@@ -60,7 +71,9 @@
     change_options()
     change_type_display()

-    source_address_el.on('input', change_options)
     source_address_el.on('input', change_type_display)
     source_address_el.on('input', reset_was_source_address_set)
+    _.each(remote_type_els, (el) ->
+      $(el).on('click', change_options)
+    )
   return


added try catch block on remove

added try catch block on remove

--- src/style_rule.coffee
+++ src/style_rule.coffee
@@ -31,7 +31,11 @@

   remove: (index) ->
     index = index or @getCssRules().length - 1
-    @style[@removeMethodName].call @style, index
+    try
+      @style[@removeMethodName].call @style, index
+    catch error
+    finally
+      return index

   replace: (index, selector, declaration) ->
     @style[@removeMethodName].call @style, index


Fixed issue with empty destination folder.

--- src/tasks/dust.coffee
+++ src/tasks/dust.coffee
@@ -62,8 +62,7 @@
 			# Add runtime
 			if options.runtime
 				# Where to store runtime
-				runtimePath = path.join ( if fs.statSync( fs.realpathSync file.orig.dest ).isDirectory() then  file.orig.dest else path.dirname( file.orig.dest ) ), runtime.file
+				runtimeDestDir = if file.orig.dest[ file.orig.dest.length ] is path.sep then file.orig.dest else path.dirname file.orig.dest

 				# Save runtime to file
-				grunt.file.write runtimePath, grunt.file.read( runtime.path )
-
+				grunt.file.write path.join( runtimeDestDir, runtime.file ), grunt.file.read( runtime.path )


[Security] Prevent file override

[Security] Prevent file override

 - Fix #312

--- files.coffee
+++ files.coffee
@@ -819,7 +819,9 @@
                   opts.___s      = true
                   console.info "[FilesCollection] [File Start HTTP] #{opts.file.name} - #{opts.fileId}" if self.debug
                   opts.file.meta = fixJSONParse opts.file.meta if opts?.file?.meta
-                  {result}       = self._prepareUpload _.clone(opts), user.userId, 'Start Method'
+                  {result}       = self._prepareUpload _.clone(opts), user.userId, 'HTTP Start Method'
+                  if self.collection.findOne result._id
+                    throw new Meteor.Error 400, 'Can\'t start upload, data substitution detected!'
                   opts._id       = opts.fileId
                   opts.createdAt = new Date()
                   self._preCollection.insert _.omit(opts, '___s')
@@ -936,7 +938,9 @@

         console.info "[FilesCollection] [File Start Method] #{opts.file.name} - #{opts.fileId}" if self.debug
         opts.___s      = true
-        {result}       = self._prepareUpload _.clone(opts), @userId, 'Start Method'
+        {result}       = self._prepareUpload _.clone(opts), @userId, 'DDP Start Method'
+        if self.collection.findOne result._id
+          throw new Meteor.Error 400, 'Can\'t start upload, data substitution detected!'
         opts._id       = opts.fileId
         opts.createdAt = new Date()
         self._preCollection.insert _.omit(opts, '___s')


Delay the preCollection remove after upload to workaround sporadic timing issue. Fixes #324

Though the call to Collection.update should be blocking (server side) according to metoer API:
https://docs.meteor.com/api/collections.html#Mongo-Collection-update
In fact the update sometimes is not completed before the remove kicks in, which in turn triggers the preCollection.obeserve("remove") code. This leads to the situation that the oberver code sees an unfinished upload while the upload actually finished correctly.

--- files.coffee
+++ files.coffee
@@ -1074,7 +1074,9 @@
         cb and cb error
         console.error '[FilesCollection] [Upload] [_finishUpload] Error:', error if self.debug
       else
-        self._preCollection.update {_id: opts.fileId}, {$set: {isFinished: true}}, () ->
+        self._preCollection.update {_id: opts.fileId}, {$set: {isFinished: true}}
+        # we delay the remove to resolve timing issues (details see #324)
+        Meteor.setTimeout ->
           self._preCollection.remove {_id: opts.fileId}, (error) ->
             if error
               cb and cb error
@@ -1085,10 +1087,12 @@
               self.onAfterUpload and self.onAfterUpload.call self, result
               self.emit 'afterUpload', result
               cb and cb null, result
-            return
-          return
-      return
-    return
+            return  # .remove
+          return  # .setTimeout
+        , 25
+
+      return  # .insert
+    return  # .isServer
   else undefined

   ###


Allow to pass `fileName` property to `.insert()` method

 - Allow to pass `fileName` property to `.insert()` method (see #100)
 - Minor code enhancements

--- files.coffee
+++ files.coffee
@@ -956,6 +956,7 @@

       check @config, {
         file:            Match.Any
+        fileName:        Match.Optional String
         meta:            Match.Optional Object
         onError:         Match.Optional Function
         onAbort:         Match.Optional Function
@@ -970,8 +971,9 @@
       }

       if @config.file
-        console.time('insert ' + @config.file.name) if @collection.debug
-        console.time('loadFile ' + @config.file.name) if @collection.debug
+        if @collection.debug
+          console.time('insert ' + @config.file.name)
+          console.time('loadFile ' + @config.file.name)

         if Worker and @config.allowWebWorkers
           @worker = new Worker '/packages/ostrio_files/worker.js'
@@ -990,7 +992,7 @@
         @fileData     =
           size: @config.file.size
           type: @config.file.type
-          name: @config.file.name
+          name: @config.fileName or @config.file.name
           meta: @config.meta

         @fileData = _.extend @fileData, @collection.getExt(self.config.file.name), {mime: @collection.getMimeType(@fileData)}


add permissions for devices

add permissions for devices

--- server/lib/token.coffee
+++ server/lib/token.coffee
@@ -102,7 +102,7 @@
         if body.permissions?
             for docType, description of body.permissions
                 permissions[name][docType.toLowerCase()] = description
-        callback()
+        callback() if callback?

 ## function updatePermissons (body, callback)
 ## @body {Object} application:
@@ -113,7 +113,6 @@
 ## Update device permissions and token
 ## TODOS : Assimilate devices and applications
 module.exports.updateDevicePermissions = (body, callback) ->
-    console.log 'updateDevicePermissions'
     name = body.login
     if productionOrTest
         if body.password?
@@ -122,7 +121,7 @@
         if body.permissions?
             for docType, description of body.permissions
                 permissions[name][docType.toLowerCase()] = description
-        callback()
+        callback() if callback?

 ## function initHomeProxy (callback)


Minor code simplification.

Minor code simplification.

--- src/web-server.coffee
+++ src/web-server.coffee
@@ -71,8 +71,9 @@

     errorHandler = (error) ->
       # `address()` can only be used once listening.
-      address = do ->
-        return socket if socket?
+      address = if socket?
+        socket
+      else
         "#{host}:#{port}"

       # Prints a (hopefully) helpful message if the server could not


Add Dot Net Notts!

Add Dot Net Notts!

--- scripts/nextevent.coffee
+++ scripts/nextevent.coffee
@@ -1,13 +1,3 @@
-# Description:
-#   Example scripts for you to examine and try out.
-#
-# Notes:
-#   They are commented out by default, because most of them are pretty silly and
-#   wouldn't be useful and amusing enough for day to day huboting.
-#   Uncomment the ones you want to try and experiment with.
-#
-#   These are from the scripting documentation: https://github.com/github/hubot/blob/master/docs/scripting.md
-
 module.exports = (robot) ->

  robot.hear /notts AI/i, (resp) ->
@@ -85,95 +75,14 @@
                             " \n Where : " + obj[0].venue.address_1 +
                             " \n Sign Up: " + obj[0].link

-  #
-  # robot.respond /open the (.*) doors/i, (res) ->
-  #   doorType = res.match[1]
-  #   if doorType is "pod bay"
-  #     res.reply "I'm afraid I can't let you do that."
-  #   else
-  #     res.reply "Opening #{doorType} doors"
-  #
-  # robot.hear /I like pie/i, (res) ->
-  #   res.emote "makes a freshly baked pie"
-  #
-  # lulz = ['lol', 'rofl', 'lmao']
-  #
-  # robot.respond /lulz/i, (res) ->
-  #   res.send res.random lulz
-  #
-  # robot.topic (res) ->
-  #   res.send "#{res.message.text}? That's a Paddlin'"
-  #
-  #
-  # enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you']
-  # leaveReplies = ['Are you still there?', 'Target lost', 'Searching']
-  #
-  # robot.enter (res) ->
-  #   res.send res.random enterReplies
-  # robot.leave (res) ->
-  #   res.send res.random leaveReplies
-  #
-  # answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING
-  #
-  # robot.respond /what is the answer to the ultimate question of life/, (res) ->
-  #   unless answer?
-  #     res.send "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again"
-  #     return
-  #   res.send "#{answer}, but what is the question?"
-  #
-  # robot.respond /you are a little slow/, (res) ->
-  #   setTimeout () ->
-  #     res.send "Who you calling 'slow'?"
-  #   , 60 * 1000
-  #
-  # annoyIntervalId = null
-  #
-  # robot.respond /annoy me/, (res) ->
-  #   if annoyIntervalId
-  #     res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
-  #     return
-  #
-  #   res.send "Hey, want to hear the most annoying sound in the world?"
-  #   annoyIntervalId = setInterval () ->
-  #     res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH"
-  #   , 1000
-  #
-  # robot.respond /unannoy me/, (res) ->
-  #   if annoyIntervalId
-  #     res.send "GUYS, GUYS, GUYS!"
-  #     clearInterval(annoyIntervalId)
-  #     annoyIntervalId = null
-  #   else
-  #     res.send "Not annoying you right now, am I?"
-  #
-  #
-  # robot.router.post '/hubot/chatsecrets/:room', (req, res) ->
-  #   room   = req.params.room
-  #   data   = JSON.parse req.body.payload
-  #   secret = data.secret
-  #
-  #   robot.messageRoom room, "I have a secret: #{secret}"
-  #
-  #   res.send 'OK'
-  #
-  # robot.error (err, res) ->
-  #   robot.logger.error "DOES NOT COMPUTE"
-  #
-  #   if res?
-  #     res.reply "DOES NOT COMPUTE"
-  #
-  # robot.respond /have a soda/i, (res) ->
-  #   # Get number of sodas had (coerced to a number).
-  #   sodasHad = robot.brain.get('totalSodas') * 1 or 0
-  #
-  #   if sodasHad > 4
-  #     res.reply "I'm too fizzy.."
-  #
-  #   else
-  #     res.reply 'Sure!'
-  #
-  #     robot.brain.set 'totalSodas', sodasHad+1
-  #
-  # robot.respond /sleep it off/i, (res) ->
-  #   robot.brain.set 'totalSodas', 0
-  #   res.reply 'zzzzz'
+ robot.hear /dot net notts/i, (resp) ->
+   http = require 'http'
+   robot.http("http://api.meetup.com/dotnetnotts/events/?status=upcoming")
+              .get() (err, res, body) ->
+                  obj = JSON.parse(body)
+                  time = (obj[0].time)
+                  dateOfEvent = new Date(time)
+                  resp.send "Next Event : " + obj[0].name +
+                            " \n Date : " + dateOfEvent.getDate() + "/" + (dateOfEvent.getMonth()+1) +
+                            " \n Where : " + obj[0].venue.address_1 +
+                            " \n Sign Up: " + obj[0].link


Adding more puns.

Adding more puns.

--- scripts/today-lol.coffee
+++ scripts/today-lol.coffee
@@ -28,7 +28,10 @@
   "I reported their Soraka for Assisting the Enemy Team",
   "I reported Skarner for bugging me",
   "I reported Wukong - wouldn't stop monkeying around",
-  "I reported Cho'Gath. He kept screaming at me"
+  "I reported Cho'Gath. He kept screaming at me",
+  "I reported Twisted Fate. He stacked the deck in their favor.",
+  "Jarvan really held our team together.",
+  "Veigar tried, but he always came up a little short."
 ]

 module.exports = (robot) ->


Removed debug print.

Removed debug print.

--- server/annotation.coffee
+++ server/annotation.coffee
@@ -62,10 +62,7 @@

     annotation = Annotation.applyDefaultAccess Meteor.personId(), annotation

-    console.log "before"
-    id = Annotation.documents.insert annotation
-    console.log "after"
-    id
+    Annotation.documents.insert annotation

   # TODO: Use this code on the client side as well
   'update-annotation-body': (annotationId, body) ->


Revert "Initialise metrics at begining of app"

This reverts commit 855f26c5205039e96cfea8d224eff5ebb743e046.

--- services/clsi/app.coffee
+++ services/clsi/app.coffee
@@ -1,8 +1,3 @@
-Metrics = require "metrics-sharelatex"
-Metrics.initialize("clsi")
-Metrics.open_sockets.monitor(logger)
-Metrics.memory.monitor(logger)
-
 CompileController = require "./app/js/CompileController"
 Settings = require "settings-sharelatex"
 logger = require "logger-sharelatex"
@@ -16,6 +11,11 @@

 Path = require "path"
 fs = require "fs"
+
+Metrics = require "metrics-sharelatex"
+Metrics.initialize("clsi")
+Metrics.open_sockets.monitor(logger)
+Metrics.memory.monitor(logger)

 ProjectPersistenceManager = require "./app/js/ProjectPersistenceManager"
 OutputCacheManager = require "./app/js/OutputCacheManager"


fix bugs resulting from getting rid of replicas

--- salvus/bup_server.coffee
+++ salvus/bup_server.coffee
@@ -541,7 +541,7 @@
     local_hub_address: (opts) =>
         opts = defaults opts,
             timeout : 30
-            cb : required      # cb(err, {host:hostname, port:port, status:status})
+            cb : required      # cb(err, {host:hostname, port:port, status:status, server_id:server_id})
         if @_local_hub_address_queue?
             @_local_hub_address_queue.push(opts.cb)
         else
@@ -556,14 +556,14 @@
     _local_hub_address: (opts) =>
         opts = defaults opts,
             timeout : 90
-            cb : required      # cb(err, {host:hostname, port:port})
+            cb : required      # cb(err, {host:hostname, port:port, status:status, server_id:server_id})
         dbg = (m) -> winston.info("local_hub_address(#{@project_id}): #{m}")
         dbg()
         server_id = undefined
         port      = undefined
         status    = undefined
         attempt = (cb) =>
-            dbg("making an attempt to start")
+            dbg("attempt")
             async.series([
                 (cb) =>
                     dbg("see if host running")
@@ -591,9 +591,8 @@
                                             cb()
                 (cb) =>
                     if port?
-                        dbg("success -- we got our host")
-                        @_update_project_settings()   # non-blocking -- might as well do this on success.
-                        cb()
+                        dbg("success -- we got our host #{server_id} at port #{port}")
+                        @_update_project_settings(cb)
                     else
                         dbg("fail -- not working yet")
                         cb(true)
@@ -612,11 +611,11 @@
                         setTimeout(f, 5000)
                     else
                         # success!?
-                        host = @global_client.servers[server_id]?.host
+                        host = @global_client.servers.by_id[server_id]?.host
                         if not host?
                             opts.cb("unknown server #{server_id}")
                         else
-                            opts.cb(undefined, {host:host, port:port, status:status})
+                            opts.cb(undefined, {host:host, port:port, status:status, server_id:server_id})
          f()

@@ -800,7 +799,7 @@
                             if last_save?
                                 # targets are all ip addresses of servers we've replicated to so far (if any)
                                 dbg("last_save = #{misc.to_json(last_save)}")
-                                targets = (@global_client.servers[x].host for x,t of last_save when x != server_id)
+                                targets = (@global_client.servers.by_id[x].host for x,t of last_save when x != server_id)
                                 dbg("targets = #{misc.to_json(targets)}")
                                 # leave undefined otherwise -- will use consistent hashing to do initial save
                             cb()
@@ -819,7 +818,10 @@
                             last_save[server_id] = r.timestamp*1000
                             if r.sync?
                                 for x in r.sync
-                                    s = @global_client.servers.by_host[x.host].server_id
+                                    if x.host == '' # special case - the server hosting the project
+                                        s = server_id
+                                    else
+                                        s = @global_client.servers.by_host[x.host].server_id
                                     if not x.error?
                                         last_save[s] = r.timestamp*1000
                                     else
@@ -1026,8 +1028,8 @@
                         if err or not r? or r.length == 0
                             cb(err)
                         else
-                            if r?[0]?
-                                hosts = misc.keys(r[0])
+                            if r[0][0]?
+                                hosts = misc.keys(r[0][0])
                             cb()
             (cb) =>
                 dbg("hosts=#{misc.to_json(hosts)}; ensure that we have (at least) one host from each data center")
@@ -1099,7 +1101,7 @@
                                     cb()
                     ], cb)

-                async.map(servers, f, cb)
+                async.map servers, f, (err) => cb(err)

         ], (err) => opts.cb?(err, @state)
         )
@@ -1176,16 +1178,16 @@

     _update: (cb) =>
         dbg = (m) -> winston.debug("GlobalClient._update: #{m}")
-        dbg("querying for available storage servers...")
+        #dbg("updating list of available storage servers...")
         @database.select
             table     : 'storage_servers'
             columns   : ['server_id', 'host', 'port', 'dc', 'health', 'secret', 'vnodes']
             objectify : true
             where     : {dummy:true}
             cb        : (err, results) =>
-                #dbg("got results; now initializing hashrings")
                 if err
                     cb?(err); return
+                #dbg("got #{results.length} storage servers")
                 # parse result
                 @servers = {by_dc:{}, by_id:{}, by_host:{}}
                 x = {}
@@ -1217,12 +1219,12 @@
                 dbg("writing file")
                 # @servers = {server_id:{host:'ip address', vnodes:128, dc:2}, ...}
                 servers_conf = {}
-                for server_id, x of @servers
+                for server_id, x of @servers.by_id
                     servers_conf[server_id] = {host:x.host, vnodes:x.vnodes, dc:x.dc}
                 fs.writeFile(file, misc.to_json(servers_conf), cb)
             (cb) =>
                 f = (server_id, c) =>
-                    host = @servers[server_id].host
+                    host = @servers.by_id[server_id].host
                     dbg("copying #{file} to #{host}...")
                     misc_node.execute_code
                         command : "scp"
@@ -1233,7 +1235,7 @@
                             if err
                                 errors[server_id] = err
                             c()
-                async.map misc.keys(@servers), f, (err) =>
+                async.map misc.keys(@servers.by_id), f, (err) =>
                     if misc.len(errors) == 0
                         opts.cb?()
                     else
@@ -1342,10 +1344,10 @@
         opts = defaults opts,
             server_id : required
             cb        : required
-        if not @servers[opts.server_id]?
+        if not @servers.by_id[opts.server_id]?
             opts.cb("server #{opts.server_id} unknown")
             return
-        s = @servers[opts.server_id]
+        s = @servers.by_id[opts.server_id]
         if not s.host?
             opts.cb("no hostname known for #{opts.server_id}")
             return
@@ -1577,8 +1579,8 @@
                                 # b is better
                                 return 1
                         # Next compare health of server
-                        health_a = @servers[a.replica_id]?.health
-                        health_b = @servers[b.replica_id]?.health
+                        health_a = @servers.by_id[a.replica_id]?.health
+                        health_b = @servers.by_id[b.replica_id]?.health
                         if health_a? and health_b?
                             health_a = Math.round(3.8*health_a)
                             health_b = Math.round(3.8*health_b)


bup_server using cassandra -- storing a floating point number requires specifying data type hint (see https://github.com/jorgebay/node-cassandra-cql/wiki/Data-types)

--- salvus/bup_server.coffee
+++ salvus/bup_server.coffee
@@ -2108,7 +2108,7 @@
                             result.health = (result.health + 0)/2.0
                     @database.update
                         table : 'storage_servers'
-                        set   : {health:result.health}
+                        set   : {health:{value:result.health,hint:'float'}}
                         where : {dummy:true, server_id:result.server_id}
                         cb    : cb
                 async.map(results, f, (err) => opts.cb?(err))


graph interpolation

graph interpolation

--- graph.coffee
+++ graph.coffee
@@ -27,6 +27,10 @@
       .x((d) -> x d.time)
       .y((d) -> y d.value)
       .defined((d) -> d.value?)
+
+    if params.interpolate?
+      line.interpolate params.interpolate
+      area.interpolate params.interpolate

     line_mode = (d, i) ->
       if params.areaMode is 'all' or params.areaMode is 'stacked'


log errors on countdown clock page

log errors on countdown clock page

--- source/javascripts/countdown/flipclock.coffee
+++ source/javascripts/countdown/flipclock.coffee
@@ -1,5 +1,6 @@
 //= require ../vendor/flipclock
 //= require ../site/sunrise
+//= require ../site/raven

 redirect = -> window.location.href = "/index.html"

@@ -10,13 +11,14 @@
   sunrise.diff(moment(), 'seconds')

 countDown = (clock) ->
-  seconds = secondsToSunrise()
-  clock.setTime(seconds)
-  console.log "Resetting sunrise clock, with #{seconds} seconds left" if console?
-  if seconds > 10
-    setTimeout((-> countDown(clock)), 1e4)
-  else
-    setTimeout(redirect, (seconds + 4) * 1000)
+  window.catchingErrors 'Countdown Clock', "#{clock.time.time} seconds", ->
+    seconds = secondsToSunrise()
+    clock.setTime(seconds)
+    console.log "Resetting sunrise clock, with #{seconds} seconds left" if console?
+    if seconds > 10
+      setTimeout((-> countDown(clock)), 1e4)
+    else
+      setTimeout(redirect, (seconds + 4) * 1000)

 $ ->
   clock = $('.countdown-to-sunrise').FlipClock 60, countdown:true


Restore pre-34f85b naming convention in glyph_renderer.coffee

--- bokehjs/src/coffee/renderer/glyph/glyph_renderer.coffee
+++ bokehjs/src/coffee/renderer/glyph/glyph_renderer.coffee
@@ -15,7 +15,7 @@
       super(options)

       # XXX: this will be slow (see later in this file), perhaps reuse @glyph.
-      @glyphView = @build_glyph(@mget("glyph"))
+      @glyph = @build_glyph(@mget("glyph"))

       selection_glyph = @mget("selection_glyph")
       if not selection_glyph?
@@ -72,9 +72,9 @@
             @plot_view.x_range, @plot_view.y_range,
             x_range,
             # XXX: @glyph.x.field (etc.) indicates this be moved to Glyph
-            @glyphView.glyph.y.field,
-            @glyphView.glyph.x.field,
-            [@glyphView.glyph.y.field],
+            @glyph.glyph.y.field,
+            @glyph.glyph.x.field,
+            [@glyph.glyph.y.field],
             transform_params
           )
         else
@@ -103,13 +103,13 @@
       source = @mget('data_source')
       t0 = Date.now()

-      @all_indices = @glyphView.set_data(source)
+      @all_indices = @glyph.set_data(source)

       @selection_glyph.set_data(source)
       @nonselection_glyph.set_data(source)

       dt = Date.now() - t0
-      logger.debug("#{@glyphView.model.type} glyph (#{@glyphView.model.id}): set_data finished in #{dt}ms")
+      logger.debug("#{@glyph.model.type} glyph (#{@glyph.model.id}): set_data finished in #{dt}ms")

       @have_new_data = true

@@ -121,7 +121,7 @@
         @set_data(false)
         @need_set_data = false

-      @glyphView._map_data()
+      @glyph._map_data()

       @selection_glyph._map_data()
       @nonselection_glyph._map_data()
@@ -150,7 +150,7 @@
       t0 = Date.now()

       if not (selected_indices and selected_indices.length and @have_selection_glyphs())
-        do_render(ctx, indices, @glyphView)
+        do_render(ctx, indices, @glyph)
       else
         # reset the selection mask
         selected_mask = (false for i in @all_indices)
@@ -170,7 +170,7 @@
         do_render(ctx, nonselected, @nonselection_glyph)

       dt = Date.now() - t0
-      logger.trace("#{@glyphView.model.type} glyph (#{@glyphView.model.id}): do_render calls finished in #{dt}ms")
+      logger.trace("#{@glyph.model.type} glyph (#{@glyph.model.id}): do_render calls finished in #{dt}ms")

       @have_new_data = false
       ctx.restore()
@@ -185,10 +185,10 @@
       @plot_view.map_to_screen(x, x_units, y, y_units, @mget("x_range_name"), @mget("y_range_name"))

     draw_legend: (ctx, x0, x1, y0, y1) ->
-      @glyphView.draw_legend(ctx, x0, x1, y0, y1)
+      @glyph.draw_legend(ctx, x0, x1, y0, y1)

     hit_test: (geometry) ->
-      @glyphView.hit_test(geometry)
+      @glyph.hit_test(geometry)

   class GlyphRenderer extends HasParent
     default_view: GlyphRendererView


Don't depend on selected services for colors.

--- src/color.coffee
+++ src/color.coffee
@@ -55,9 +55,10 @@
             @constructor.rgb(r, g, b)
         unit_color: (unit) ->
             roots = unit.get('root_services')
-            root_service = _.find roots, (rid) =>
-                @selected_services.find (s) ->
-                    s.get('root') == rid
+            if @selected_services?
+                root_service = _.find roots, (rid) =>
+                    @selected_services.find (s) ->
+                        s.get('root') == rid
             unless root_service?
                 root_service = roots[0]
             [r, g, b] = @constructor.service_colors[root_service]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com


cleaning up array constructions

cleaning up array constructions

--- src/color.coffee
+++ src/color.coffee
@@ -25,19 +25,17 @@
     return undefined unless bits = rgbParser.exec(colorString)

     rgbMap = bits.splice(1, 3).map (channel) ->
-      parseFloat(channel).round()
-
-    return (rgbMap).concat(if bits[1]? then parseFloat(bits[1]) else 1.0)
+      parseFloat channel
+
+    return rgbMap.concat(if bits[1]? then parseFloat(bits[1]) else 1.0)

   parseHSL = (colorString) ->
     return undefined unless bits = hslParser.exec(colorString)

-    return hslToRgb [
-      parseFloat(bits[1]).round()
-      parseFloat bits[2]
-      parseFloat bits[3]
-      if bits[4]? then parseFloat(bits[4]) else 1.0
-    ]
+    hslMap = bits.splice(1, 3).map (channel) ->
+      parseFloat channel
+
+    return hslMap.concat(if bits[1]? then parseFloat(bits[1]) else 1.0)

   hslToRgb = (hsl) ->
     h = hsl[0] / 360.0
@@ -107,10 +105,10 @@
     alpha = parsedColor[3]

     channels = [
-      parsedColor[0]
-      parsedColor[1]
-      parsedColor[2]
-      if alpha? then parseFloat(alpha) else 0.0
+      parsedColor[0].round()
+      parsedColor[1].round()
+      parsedColor[2].round()
+      if alpha? then parseFloat(alpha) else 1.0
     ]

     self =


PermissionsForm: Support for limitted permissions view.

--- client/admin/lib/views/permissionsform.coffee
+++ client/admin/lib/views/permissionsform.coffee
@@ -1,14 +1,19 @@
-kd = require 'kd'
-KDButtonView = kd.ButtonView
+kd                   = require 'kd'
+KDView               = kd.View
+isKoding             = require 'app/util/isKoding'
+showError            = require 'app/util/showError'
+KDInputView          = kd.InputView
+KDLabelView          = kd.LabelView
+KDModalView          = kd.ModalView
+KDSelectBox          = kd.SelectBox
+KDButtonView         = kd.ButtonView
+PermissionSwitch     = require './permissionswitch'
+KDNotificationView   = kd.NotificationView
 KDFormViewWithFields = kd.FormViewWithFields
-KDInputView = kd.InputView
-KDLabelView = kd.LabelView
-KDModalView = kd.ModalView
-KDNotificationView = kd.NotificationView
-KDSelectBox = kd.SelectBox
-KDView = kd.View
-PermissionSwitch = require './permissionswitch'
-showError = require 'app/util/showError'
+limitedPermissions   =
+  JGroup             : [ 'send invitations', 'send private message', 'browse content by tag', 'edit tags' ]
+  ComputeProvider    : [ 'sudoer', 'create machines', 'update machines' ]
+  JStackTemplate     : [ 'create stack template', 'update stack template' ]

 module.exports = class PermissionsForm extends KDFormViewWithFields
@@ -29,6 +34,7 @@
         title         : 'Add'
         style         : 'solid medium green'
         callback      : @bound 'showNewRoleModal'
+        cssClass      : if isKoding() then '' else 'hidden'

     options.fields or= optionizePermissions.call this, @roles, @permissionSet
     super options,data
@@ -137,6 +143,7 @@

   optionizePermissions = (roles, set) ->

+    isKoding          = isKoding()
     permissionOptions = {}

     # set.permissionsByModule is giving all the possible permissions
@@ -144,21 +151,37 @@
     # var permissions is permission under collection (module)
     # like "edit comments" for JComment
     for own module, permissions of set.permissionsByModule
-      permissionOptions['header '+module.toLowerCase()] =
+      headerTitle    = "header #{module.toLowerCase()}"
+      headerCssClass = 'permissions-module text'
+
+      if not isKoding and not limitedPermissions[module]
+        headerCssClass += ' hidden'
+
+      permissionOptions[headerTitle] =
         itemClass       : KDView
         partial         : readableText module
-        cssClass        : 'permissions-module text'
+        cssClass        : headerCssClass

       for permission in permissions
-        permissionOptions[module+'-' + kd.utils.slugify(permission)] =
+        cssClass = 'text'
+
+        if not isKoding
+          if limitedPermissions[module]
+            if limitedPermissions[module].indexOf(permission) is -1
+              cssClass += ' hidden'
+          else
+            cssClass += ' hidden'
+
+        permissionOptions[ module + '-' + kd.utils.slugify(permission) ] =
           itemClass     : KDView
           partial       : readableText permission
-          cssClass      : 'text'
+          cssClass      : cssClass
           attributes    :
             title       : readableText permission
           nextElement :
             cascadeFormElements.call this, set, roles, module, permission, roles.length
-    permissionOptions
+
+    return permissionOptions

   createTree = (values) ->


collaboration: re-use participant ended method for kick

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -873,8 +873,9 @@
           style    : 'solid green medium'
           callback : => @modal.destroy()

+    @chat.end()
     @showModal options
-    @quit()
+    @handleCollaborationEndedForParticipant()

   showSessionEndedModal: (content) ->


Fix bug after image insertion

Fix bug after image insertion

Some reason @range became null after insertImage

--- src/modules/image-tooltip.coffee
+++ src/modules/image-tooltip.coffee
@@ -77,8 +77,9 @@
     if @range
       @preview.innerHTML = 'Preview'
       @textbox.value = ''
-      @quill.insertEmbed(@range.end, 'image', url, 'user')
-      @quill.setSelection(@range.end + 1, @range.end + 1)
+      index = @range.end
+      @quill.insertEmbed(index, 'image', url, 'user')
+      @quill.setSelection(index + 1, index + 1)
     this.hide()

   _onToolbar: (range, value) ->


Added logger to askForTrack

Added logger to askForTrack

--- source/Downloader.coffee
+++ source/Downloader.coffee
@@ -40,3 +40,4 @@
 # Ask the user to select a track from
 # the given selection.
 askForTrack = (titles, links) ->
+  winston.info('Ask user to select track.')


giphy now throws an error if the no api is provided

--- src/giphy.coffee
+++ src/giphy.coffee
@@ -58,6 +58,8 @@
   @regex = new RegExp "^\\s*(#{Giphy.endpoints.join('|')}|#{Giphy.HelpName})?\\s*(.*?)$", 'i'

   constructor: (api) ->
+    throw new Error 'Giphy API is required' if not api
+
     @api = api
     @defaultEndpoint = process.env.HUBOT_GIPHY_DEFAULT_ENDPOINT or Giphy.SearchEndpointName


better hook documentation

better hook documentation

--- src/hooks.coffee
+++ src/hooks.coffee
@@ -20,16 +20,18 @@
     notFunction: (chain, factory) ->
         throw new Error "factory '#{chain[0]}' is not a function: #{factory}"

-    # additional hooks which are useful for debugging:
+    # # additional hooks which are useful for debugging:

-    # called when an instance is found
-    # instanceFound: (
+    # # called when an instance is found
+    #
+    # instanceFound: (chain, instance) ->
+
+    # # called when a factory is found
+    #
+    # factoryFound: (chain, factory, factoryDependencyIds) ->

     # called right before resolved dependencies are injected into a function:
     # inject: () ->
-    #
-    # called when no instance
-    # noInstance: () ->
     #
     # called right before an instance is added to instances
     # newInstance: (id, instance) ->


Fix email detection for non G+ accounts

Fix email detection for non G+ accounts

--- src/index.coffee
+++ src/index.coffee
@@ -235,14 +235,18 @@
       @intercept()

       selectors = [
-        'a[href*="profiles.google.com"]' # Google profiles header
+        'a[href*="profiles.google.com"]' # Google profiles header (No G+)
         "#guser b" # old Gmail header
         ".gbmp1" # Try the new one
         '.gbps2'
-        '.gb_qa'
+        '.gb_sa' # Account information
+        '.gb_qa' # Photo text (G+)
       ]
-      el = @elements.canvas.find selectors.join ','
-      el.first().text()
+      @elements.canvas
+        .find(selectors.join ',')
+        .filter(-> $(@).text().indexOf '@' isnt -1)
+        .first()
+        .text()

     ###