Implement bracket forwarding for addressable outports

--- src/lib/Component.coffee
+++ src/lib/Component.coffee
@@ -222,14 +222,31 @@
       do @processOutputQueue
     return

+  addToResult: (result, port, ip, before = false) ->
+    if port.indexOf('[') is -1
+      # Regular port
+      outport = port
+    else
+      [port, outport, idx] = port.match /(.*)\[([0-9+])\]/
+    method = if before then 'unshift' else 'push'
+    if @outPorts[outport].isAddressable()
+      idx = if idx then parseInt(idx) else ip.index
+      result[outport] = {} unless result[outport]
+      result[outport][idx] = [] unless result[outport][idx]
+      ip.index = idx
+      result[outport][idx][method] ip
+      return
+    result[outport] = [] unless result[outport]
+    result[outport][method] ip
+
   addBracketForwards: (result) ->
     if result.__bracketClosingBefore?.length
       for context in result.__bracketClosingBefore
         debug "#{@nodeId} closeBracket-A #{context.closeIp.data} #{context.ports}"
         continue unless context.ports.length
         for port in context.ports
-          result[port] = [] unless result[port]
-          result[port].unshift context.closeIp.clone()
+          ipClone = context.closeIp.clone()
+          @addToResult result, port, ipClone, true

     if result.__bracketContext
       # First see if there are any brackets to forward. We need to reverse
@@ -239,6 +256,21 @@
         return unless context.length
         for outport, ips of result
           continue if outport.indexOf('__') is 0
+          if @outPorts[outport].isAddressable()
+            for idx, idxIps of ips
+              portIdentifier = "#{outport}[#{idx}]"
+              unforwarded = context.filter (ctx) =>
+                return false unless @isForwardingOutport inport, outport
+                ctx.ports.indexOf(portIdentifier) is -1
+              continue unless unforwarded.length
+              unforwarded.reverse()
+              for ctx in unforwarded
+                ipClone = ctx.ip.clone()
+                ipClone.index = parseInt idx
+                idxIps.unshift ipClone
+                debug "#{@nodeId} register #{portIdentifier} to #{inport} ctx #{ctx.ip.data}"
+                ctx.ports.push portIdentifier
+            continue
           unforwarded = context.filter (ctx) =>
             return false unless @isForwardingOutport inport, outport
             ctx.ports.indexOf(outport) is -1
@@ -254,8 +286,8 @@
         debug "#{@nodeId} closeBracket-B #{context.closeIp.data} #{context.ports}"
         continue unless context.ports.length
         for port in context.ports
-          result[port] = [] unless result[port]
-          result[port].push context.closeIp.clone()
+          ipClone = context.closeIp.clone()
+          @addToResult result, port, ipClone, false

     delete result.__bracketClosingBefore
     delete result.__bracketContext
@@ -268,14 +300,29 @@
       @addBracketForwards result
       for port, ips of result
         continue if port.indexOf('__') is 0
+        if @outPorts.ports[port].isAddressable()
+          for idx, idxIps of ips
+            idx = parseInt idx
+            continue unless @outPorts.ports[port].isAttached idx
+            for ip in idxIps
+              portIdentifier = "#{port}[#{ip.index}]"
+              if ip.type is 'openBracket'
+                debug "#{@nodeId} sending < #{ip.data} to #{portIdentifier}"
+              else if ip.type is 'closeBracket'
+                debug "#{@nodeId} sending > #{ip.data} to #{portIdentifier}"
+              else
+                debug "#{@nodeId} sending DATA to #{portIdentifier}"
+              @outPorts[port].sendIP ip
+          continue
         continue unless @outPorts.ports[port].isAttached()
         for ip in ips
+          portIdentifier = port
           if ip.type is 'openBracket'
-            debug "#{@nodeId} sending < #{ip.data} to #{port}"
+            debug "#{@nodeId} sending < #{ip.data} to #{portIdentifier}"
           else if ip.type is 'closeBracket'
-            debug "#{@nodeId} sending > #{ip.data} to #{port}"
+            debug "#{@nodeId} sending > #{ip.data} to #{portIdentifier}"
           else
-            debug "#{@nodeId} sending DATA to #{port}"
+            debug "#{@nodeId} sending DATA to #{portIdentifier}"
           @outPorts[port].sendIP ip
       @outputQ.shift()

@@ -543,8 +590,7 @@
     ip.scope = @scope if @scope isnt null and ip.scope is null

     if @nodeInstance.isOrdered()
-      @result[port] = [] unless port of @result
-      @result[port].push ip
+      @nodeInstance.addToResult @result, port, ip
       return
     @nodeInstance.outPorts[port].sendIP ip

@@ -594,8 +640,13 @@
     @error error if error

     isLast = =>
-      pos = @nodeInstance.outputQ.indexOf @result
-      len = @nodeInstance.outputQ.length
+      resultsOnly = @nodeInstance.outputQ.filter (q) ->
+        return true unless q.__resolved
+        if Object.keys(q).length is 2 and q.__bracketClosingAfter
+          return false
+        true
+      pos = resultsOnly.indexOf @result
+      len = resultsOnly.length
       load = @nodeInstance.load
       return true if pos is len - 1
       return true if pos is -1 and load is len + 1
@@ -608,6 +659,7 @@
       for port, contexts of @nodeInstance.bracketContext
         continue unless contexts[@scope]
         nodeContext = contexts[@scope]
+        continue unless nodeContext.length
         context = nodeContext[nodeContext.length - 1]
         buf = @nodeInstance.inPorts[context.source].getBuffer context.ip.scope
         loop


Fix for mail links

Fix for mail links

--- src/core/windowcontroller.coffee
+++ src/core/windowcontroller.coffee
@@ -104,9 +104,10 @@
       if nearestLink?.target?.length is 0 # links with a target attribute should work as normal.
         href   = nearestLink.getAttribute "href"
         isHttp = href?.indexOf("http") is 0
+        isMail = href?.indexOf("mailto") is 0
         if isHttp
           nearestLink.target = "_blank"
-        else
+        else if not isMail
           e.preventDefault()
           if href and not /^#/.test href
             KD.getSingleton("router").handleRoute href


implement registering of tags that need their contents passed as raw text; used by `<mkts-table>`


linkedin connector refactoring

linkedin connector refactoring

--- server/konnectors/linkedin.coffee
+++ server/konnectors/linkedin.coffee
@@ -14,6 +14,89 @@
 CompareContacts = require '../lib/compare_contacts'

 ACCOUNT_TYPE = 'com.linkedin'
+
+
+## Helpers
+
+linkedin =
+
+    # Extract phone numbers from given linkedin data structure.
+    getPhoneNumber: (data) ->
+        listPhones = []
+
+        data.phone_numbers?.forEach (number) ->
+            listPhones.push
+                name: 'tel'
+                type: number.type.toLowerCase()
+                value: number.number.replace(/ /g, '')
+        listPhones
+
+
+    # Extract emails from given linkedin data structure.
+    getEmails: (data) ->
+        listEmails = []
+
+        data.emails_extended?.forEach (email) ->
+            listEmails.push
+                name: 'email'
+                value: email.email
+                type: 'internet'
+                pref: email.primary is true ? true : undefined
+        listEmails
+
+
+    # Extract urls from given linkedin data structure.
+    getUrls: (data) ->
+        listUrls = []
+
+        data.sites?.forEach (site) ->
+            listUrls.push
+                name: 'url'
+                value: site.url
+                type: site.name
+
+        data.profiles?.forEach (profile) ->
+            listUrls.push
+                name: 'url'
+                value: profile.url
+                type: 'linkedin'
+
+        data.twitter?.forEach (twitter) ->
+            listUrls.push
+                name: 'url'
+                value: twitter.url
+                type: 'twitter'
+
+        listUrls
+
+
+    # Currently there isn't a good address parser. So all the addresses are set
+    # in the locality fields. The Linkedin API defines some fields
+    # precisely but we can only handle properly the country. That's why this
+    # function keeps only the country.
+    # When we'll have a correct address parser we'll be able to include
+    # region and locality.
+    getAddresses: (data) ->
+        listAddresses = []
+
+        if data.location?
+            segmentAddress = data.location.split(', ')
+                .reverse()
+            country = segmentAddress[0] or ''
+            #region = segmentAddress[1] or ''
+            #locality = segmentAddress[2] or ''
+
+        data.addresses?.forEach (address) =>
+            addressArray = ContactHelper.adrStringToArray address.raw
+            addressArray[6] = country
+
+            listAddresses.push
+                name: 'adr'
+                value: addressArray
+                type: 'main'
+
+        listAddresses
+

 module.exports =
@@ -50,7 +133,6 @@
 # The html parsing is done with cheerio, a "jquery like" that can be run on
 # the server side.
 retrieveTokens = (requiredFields, entries, data, next) ->
-    log.info 'Retrieve Tokens'
     opts =
         url: 'https://linkedin.com'
         jar: true
@@ -65,7 +147,7 @@
             $ = cheerio.load body
             entries.csrfToken = $('#loginCsrfParam-login').val()
             entries.accountName = requiredFields.login
-            log.info 'CSRF Token retrieved successfully'
+            log.info 'CSRF Token retrieved successfully.'
             next()

@@ -96,7 +178,6 @@
 # Retrieve a list of all Linkedin ID contacts available. The linkedin ID will
 # be used to retrieve additional data about the contact.
 retrieveContactList = (requiredFields, entries, data, next) ->
-    log.info 'Retrieve list contact'

     contactsUrl = "https://www.linkedin.com/contacts/api/contacts/"
     contactsUrl += "?start=0&count=10000&fields=id"
@@ -105,12 +186,13 @@
         jar: true
         json: true

-    log.info 'Retrieve contact list...'
+    log.info 'Retrieving contact list...'
     request.get opts, (err, res, body) ->
         return next err if err

         if body.status? and body.status is 'error'
             next new Error body.status_details
+
         else
             entries.listContacts = body.contacts
             if not entries.listContacts?
@@ -136,221 +218,148 @@
         return next err if err

         entries.cozyContactsByFn = {}
-        entries.ofAccountByIds = {}
+        entries.cozyContactsByAccountIds = {}
         for contact in contacts
             entries.cozyContactsByFn[contact.fn] = contact
             account = contact.getAccount ACCOUNT_TYPE, entries.accountName
-            entries.ofAccountByIds[account.id] = contact if account?
+            entries.cozyContactsByAccountIds[account.id] = contact if account?

         log.info 'Cozy contacts contacts loaded.'
         next()

-# Create or retrieve a specific to Linkedin, then
-# make a request for each contact retrieved in the previous request
-# (retrieveListContact) with all data needed.
-#
-# In order to reduce the waiting betwen each request an async queue
-# is create. It process the 10 first contacts. (see Async lib)
+# Retrieve additional data from Linkedin by making a request for each contact
+# retrieved in previous step. Then it saves or updates the contact in the
+# database.
 saveContacts = (requiredFields, entries, data, next) ->

-    processRetrievingContactData = (contactId, done) ->
-        contacts = """https://www.linkedin.com/contacts/api/contacts/\
-        #{contactId}/?fields=name,first_name,last_name,\
-        emails_extended,phone_numbers,sites,addresses,\
-        company,title,geo_location,profiles,twitter,tag,\
-        secure_profile_image_url"""
-
-        opts =
-            url: contacts
-            jar: true
-            json: true
-
-        request.get opts, (err, res, body) ->
-            return done err if err
-            if body.status? and body.status is 'error'
-                return done new Error(body.status_details)
-
-            datapoints = []
-
-            # Fill datapoints
-            data = body.contact_data
-            datapoints = datapoints.concat getPhoneNumber(data)
-            datapoints = datapoints.concat getEmails(data)
-            datapoints = datapoints.concat getUrls(data)
-            datapoints = datapoints.concat getAddresses(data)
-
-            # Contact data composition
-            finalContact = new Contact
-                n: "#{data.last_name};#{data.first_name}"
-                fn: data.name
-                title: data.title || undefined
-                org: data.company?.name || undefined
-                title: data.title || undefined
-                tags: ['linkedin']
-                datapoints: datapoints || undefined
-
-            finalContact.imageUrl = data.secure_profile_image_url || undefined
-            ContactHelper.setAccount finalContact,
-                type: ACCOUNT_TYPE
-                name: entries.accountName
-                id: data.id
-
-            done null, finalContact
-
-
-    # Create the queue
-    queue = async.queue processRetrievingContactData, 10
-
-    # Is executed once all data have been proceed
-    queue.drain = ->
-        log.info 'All data retrieved'
-        next()
-
-    Tag.getOrCreate { name: 'linkedin', color: '#1B86BC'}, (err, tag) ->
-        if err
-            entries.tag = null
+    Tag.getOrCreate {name: 'linkedin', color: '#1B86BC'}, (err, tag) ->
+        return next err if err
+
+        entries.tag = tag
+
+        processLinkedinContact = (contact, done) ->
+            contactUrl = """https://www.linkedin.com/contacts/api/contacts/\
+            #{contact.id}/?fields=name,first_name,last_name,\
+            emails_extended,phone_numbers,sites,addresses,\
+            company,title,geo_location,profiles,twitter,tag,\
+            secure_profile_image_url"""
+
+            opts =
+                url: contactUrl
+                jar: true
+                json: true
+
+            request.get opts, (err, res, body) ->
+                return done err if err
+                if body.status? and body.status is 'error'
+                    return done new Error body.status_details
+
+                datapoints = []
+
+                # Fill datapoints
+                data = body.contact_data
+                datapoints = datapoints.concat linkedin.getPhoneNumber data
+                datapoints = datapoints.concat linkedin.getEmails data
+                datapoints = datapoints.concat linkedin.getUrls data
+                datapoints = datapoints.concat linkedin.getAddresses data
+
+                # Contact data composition
+                newCozyContact = new Contact
+                    n: "#{data.last_name};#{data.first_name}"
+                    fn: data.name
+                    org: data.company?.name
+                    title: data.title
+                    tags: ['linkedin']
+                    datapoints: datapoints
+
+                # TODO ensure it could not be set via the constructor.
+                newCozyContact.imageUrl = data.secure_profile_image_url
+
+                # Set information source for the given contact. It adds a flag
+                # to say that the contact comes from Linkedin.
+                ContactHelper.setAccount newCozyContact,
+                    type: ACCOUNT_TYPE
+                    name: entries.accountName
+                    id: data.id
+
+                saveContact newCozyContact, entries, done
+
+        contacts = entries.listContacts
+        async.eachSeries contacts, processLinkedinContact, (err) ->
+            log.info 'All linkedin contacts were processed.'
+            next()
+
+
+# Saves contact information to the Data System. If the contact doesn't already
+# exist, it is created. If the contact exists, it's updated with the Linkedin
+# data.
+saveContact  = (newContact, entries, callback) ->
+    return callback() if not newContact?
+
+    linkAccount = ContactHelper.getAccount(
+        newContact, ACCOUNT_TYPE, entries.accountName
+    )
+    imageUrl = newContact.imageUrl
+    delete newContact.imageUrl
+
+    # Case where the contact already exists and where it was imported from
+    # Linkedin.
+    if entries.cozyContactsByAccountIds[newContact.id]?
+        cozyContact = entries.cozyContactsByAccountIds[linkAccount.id]
+        cozyAccount = cozyContact.getAccount ACCOUNT_TYPE, entries.accountName
+        newRev = ContactHelper.intrinsicRev newContact
+        previousRev = ContactHelper.intrinsicRev cozyContact
+
+        if newRev isnt previousRev
+            log.info "Update #{cozyContact.fn} with LinkedIn data."
+            updateContact cozyContact, newContact, imageUrl, callback
+
+        # Already up to date, nothing to do.
         else
-            entries.tag = tag
-
-        # Add all contact Id to the queue
-        entries.listContacts.forEach (contact) ->
-            queue.push contact.id, (err, finalContact) ->
-                log.error err if err
-                saveContact finalContact, entries
-
-
-# Extract numbers from given linkedin data structure.
-getPhoneNumber = (data) ->
-    listPhones = []
-
-    data.phone_numbers?.forEach (number) ->
-        listPhones.push
-            name: 'tel'
-            type: number.type.toLowerCase()
-            value: number.number.replace(/ /g, '')
-    listPhones
-
-
-# Extract emails from given linkedin data structure.
-getEmails = (data) ->
-    listEmails = []
-
-    data.emails_extended?.forEach (email) ->
-        listEmails.push
-            name: 'email'
-            value: email.email
-            type: 'internet'
-            pref: email.primary is true ? true : undefined
-    listEmails
-
-
-# Extract urls from given linkedin data structure.
-getUrls = (data) ->
-    listUrls = []
-
-    data.sites?.forEach (site) ->
-        listUrls.push
-            name: 'url'
-            value: site.url
-            type: site.name
-
-    data.profiles?.forEach (profile) ->
-        listUrls.push
-            name: 'url'
-            value: profile.url
-            type: 'linkedin'
-
-    data.twitter?.forEach (twitter) ->
-        listUrls.push
-            name: 'url'
-            value: twitter.url
-            type: 'twitter'
-
-    listUrls
-
-
-# Currently there isn't an address parser so all the addresses are set in the
-# locality fields. The Linkedin API allow us to define some fields precisely but
-# that can lead to a duplicate so only the country is specify in the right fields
-# and we should wait an address parser before use them
-getAddresses = (data) ->
-    listAddresses = []
-
-    if data.location?
-        segmentAddress = data.location.split(', ')
-            .reverse()
-        country = segmentAddress[0] || ''
-        #region = segmentAddress[1] || ''
-        #locality = segmentAddress[2] || ''
-
-    data.addresses?.forEach (address) =>
-        addressArray = ContactHelper.adrStringToArray address.raw
-        addressArray[6] = country
-
-        listAddresses.push
-            name: 'adr'
-            value: addressArray
-            type: 'main'
-
-    listAddresses
-
-
-# Save and/or merge the imported contacts with the existing
-saveContact  = (linkContact, entries) ->
-    linkAccount = ContactHelper.getAccount linkContact, ACCOUNT_TYPE, entries.accountName
-
-
-    urlImage = linkContact.imageUrl
-    delete linkContact.imageUrl
-
-    endSavePicture = (err, updatedContact) ->
-        if err?
-            log.error "An error occured while creating or updating the " + \
-                      "contact."
-            log.raw err
-            done err
-
-
-        if urlImage?
-            opts = url.parse(urlImage)
-            https.get opts, (stream) ->
-                stream.on 'error', (err) -> log.error err
-
-                updatedContact.attachFile stream, {name: 'picture'}, (err)->
-                    if err
-                        log.error "picture #{err}"
-                    else
-                        log.debug "picture ok"
-
-
-    updateContact = (fromCozy, fromLinkedin) ->
-        CompareContacts.mergeContacts fromCozy, fromLinkedin
-        fromCozy.save endSavePicture
-
-
-    if not linkAccount?
-        throw new Error "Contact Account not created:"
-
-    if entries.ofAccountByIds[linkAccount.id]?
-        cozyContact = entries.ofAccountByIds[linkAccount.id]
-        cozyAccount = cozyContact.getAccount ACCOUNT_TYPE, entries.accountName
-        if ContactHelper.intrinsicRev(linkContact) isnt ContactHelper.intrinsicRev(cozyContact)
-            log.info "Update #{cozyContact.fn} from LinkedIn"
-            log.debug "Update #{linkContact.fn} from LinkedIn"
-            updateContact cozyContact, linkContact
-
-        else # Already uptodate, nothing to do.
-            log.info "LinkedIn contact #{cozyContact.fn} already synced and uptodate"
-            log.debug "LContact #{linkContact.n}already synced and uptodate"
-
+            log.info "LinkedIn contact #{cozyContact.fn} is up to date."
+            callback()
+
+    # Case where the contact already exists but was not imported from Linkedin.
+    else if entries.cozyContactsByFn[newContact.fn]?
+        cozyContact = entries.cozyContactsByFn[newContact.fn]
+        log.info "Link #{cozyContact.fn} to linkedin account and update data."
+        updateContact cozyContact, newContact, imageUrl, callback
+
+    # Case where the contact is not listed in the database.
     else
-        if linkContact?
-            if entries.cozyContactsByFn[linkContact.fn]?
-                if cozyContact?
-                    log.info "Link #{cozyContact.fn} to linkedin account"
-                    updateContact cozyContact, linkContact
-            else
-                log.info "Create #{linkContact.fn} contact"
-                Contact.create linkContact, endSavePicture
-
+        log.info "Create new contact for #{newContact.fn}."
+        Contact.create newContact, (err, newContact) ->
+            return callback err if err
+            savePicture newContact, imageUrl, callback
+
+
+# Update contact with information coming from Linkedin, picture included.
+updateContact = (fromCozy, fromLinkedin, imageUrl, callback) ->
+    CompareContacts.mergeContacts fromCozy, fromLinkedin
+    fromCozy.save (err) ->
+        return callback err if err
+        savePicture fromCozy, imageUrl, callback
+
+
+# Change contact picture with the one coming from Linkedin.
+savePicture = (cozyContact, imageUrl, callback) ->
+    if imageUrl?
+        opts = url.parse imageUrl
+        https.get opts, (stream) ->
+
+            stream.on 'error', (err) ->
+                log.error err
+
+            cozyContact.attachFile stream, {name: 'picture'}, (err) ->
+                if err
+                    log.error """
+                    Error occured while saving picture for #{cozyContact.fn}.
+                    """
+                    log.raw err
+                else
+                    log.info "Picture successfully saved for #{cozyContact.fn}."
+                callback()
+    else
+        callback()
+


LightweightKoding: Removed log line.

LightweightKoding: Removed log line.

--- server/lib/crawler/helpers.coffee
+++ server/lib/crawler/helpers.coffee
@@ -70,7 +70,6 @@
     return tagContent

 createActivityContent = (JAccount, model, comments, createFullHTML=no, putBody=yes, callback) ->
-  # console.log "model", model
   {Relationship} = require 'jraphical'
   {htmlEncode}   = require 'htmlencode'
   marked         = require 'marked'


Style

Style

--- server/packages/user/login.coffee
+++ server/packages/user/login.coffee
@@ -32,10 +32,7 @@

 						(Promise.promisify req.login, req) @user

-					).then(->
-
-						@user.redactFor @user
-					)
+					).then -> @user.redactFor @user

 # If no user is found, the rejection will happen a lot sooner than if one is;
 # password hashing must be done in the latter case. We'll create an artificial


Store GeoCENS JS object

Store GeoCENS JS object

--- app/assets/javascripts/workbench/workbench/models/sensor.js.coffee
+++ app/assets/javascripts/workbench/workbench/models/sensor.js.coffee
@@ -15,8 +15,9 @@
         sensor_id: @id
         done: (sensor) =>
             @set(sensor.metadata)
+            @object = sensor
             @trigger("sensorLoaded")
-            @get("datastreams").fetch(sensor: sensor)
+            @get("datastreams").fetch(sensor: @object)
     return this

 class Workbench.Collections.SensorsCollection extends Backbone.Collection


reestablish connec! cache clear on widget refresh

--- src/components/widgets-common/top-buttons/top-buttons.directive.coffee
+++ src/components/widgets-common/top-buttons/top-buttons.directive.coffee
@@ -23,7 +23,7 @@

   $scope.refreshWidget = ->
     w.isLoading = true
-    ImpacWidgetsSvc.show(w).finally(-> w.isLoading=false)
+    ImpacWidgetsSvc.show(w, true).finally(-> w.isLoading=false)

   $scope.toggleEditMode = ->
     if !w.isLoading


bring down migration timeout in delete doc lines to  0ms

--- server-ce/migrations/2_doc_lines_delete_from_project.coffee
+++ server-ce/migrations/2_doc_lines_delete_from_project.coffee
@@ -167,7 +167,7 @@
 							markProjectAsProcessed project_id, (err)->
 								setTimeout(
 									-> callback(err)
-								,5000)
+								,0)

 exports.migrate = (client, done = ->)->
 	getProjectIds (err, ids)->


clean up options handling

clean up options handling

--- services/web/app/coffee/Features/Compile/ClsiManager.coffee
+++ services/web/app/coffee/Features/Compile/ClsiManager.coffee
@@ -17,9 +17,10 @@
 module.exports = ClsiManager =

 	sendRequest: (project_id, user_id, options = {}, callback) ->
-		ClsiManager.sendRequestOnce project_id, user_id, _.clone(options), (error, status, result...) ->
+		ClsiManager.sendRequestOnce project_id, user_id, options, (error, status, result...) ->
 			return callback(error) if error?
 			if status is 'conflict'
+				options = _.clone(options)
 				options.syncType = "full" #  force full compile
 				ClsiManager.sendRequestOnce project_id, user_id, options, callback # try again
 			else
@@ -147,6 +148,7 @@
 					path = docPath[doc._id]
 					docs[path] = doc
 				# send new docs but not files as those are already on the clsi
+				options = _.clone(options)
 				options.syncType = "incremental"
 				options.syncState = projectStateHash
 				ClsiManager._finaliseRequest project_id, options, project, docs, [], callback
@@ -154,6 +156,7 @@
 	_buildRequestFromMongo: (project_id, options, project, projectStateHash, callback = (error, request) ->) ->
 		ClsiManager._getContentFromMongo project_id, (error, docs, files) ->
 			return callback(error) if error?
+			options = _.clone(options)
 			options.syncType = "full"
 			options.syncState = projectStateHash
 			ClsiManager._finaliseRequest project_id, options, project, docs, files, callback


uncomment nested branch fix re-pairing. Only re-pair after unwrapping all nested branches

--- src/methods/Rules.coffee
+++ src/methods/Rules.coffee
@@ -126,7 +126,7 @@
         unless old == undefined
           @queries.clean(@getContinuation(path) , continuation, operation.parent, scope)
         unless @switching
-          switching = true#@switching = true
+          switching = @switching = true

         @queries[path] = condition
         if switching


define form

define form

--- MediaAdminBundle/Resources/public/coffee/view/CropFormView.coffee
+++ MediaAdminBundle/Resources/public/coffee/view/CropFormView.coffee
@@ -135,7 +135,8 @@
     $('#crop-group').hide()
     $('.media_crop_preview img', @$el).hide()
     $('#image-loader').show()
-    $(e.currentTarget).ajaxSubmit
+    form = $(e.currentTarget)
+    form.ajaxSubmit
       url: form.data('action')
       statusCode:
         200: () ->
@@ -148,7 +149,8 @@
     $('.media-override-format-form').hide()
     $('.media_crop_preview img', @$el).hide()
     $('#image-loader').show()
-    $(e.currentTarget).ajaxSubmit
+    form = $(e.currentTarget)
+    form.ajaxSubmit
       url: form.data('action')
       statusCode:
         200: () ->


allowing attribute based binding

allowing attribute based binding

--- src/mixins/detail.coffee
+++ src/mixins/detail.coffee
@@ -96,11 +96,11 @@
     @getSubPanel().find(selector).each (idx, e) =>
       el = @.$(e)
       action = el.attr("data-#{name}") or "default"
-      # args = action.split(":")
-      # action = args[0]
-      # args = args[1..]
+      args = action.split(":")
+      action = args[0]
+      args = args[1..]
       func = @dataActions[action]
-      opts = {el: el, name: name, value: @model.get(name)}#, args: args}
+      opts = {el: el, name: name, value: @model.get(name), args: args}
       if func
         func.bind(@)(opts)
       else


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- source/javascripts/_cornerstone/string_extensions.js.coffee
+++ source/javascripts/_cornerstone/string_extensions.js.coffee
@@ -202,7 +202,7 @@
   this.replace(/\.[^\.]*$/, '')

 String::toInt = ->
-
+  parseInt(this, 10)

 String::parseHex = ->
   hexString = @replace(/#/, '')


Async tree (MSIE6 is slooow)

Async tree (MSIE6 is slooow)

--- src/gui/tabs/remote/index.coffee
+++ src/gui/tabs/remote/index.coffee
@@ -6,7 +6,12 @@

 exports.a = (pane)->
   exports.a = 0
-  echo "Building remote Node.js version tree"
+  echo s = "Building remote Node.js version tree..."
+  pane.innerHTML = s
+  setTimeout render
+
+render = ->
+  pane = exports.$d
   # Load remotes list without fetch (previously loaded)
   pane.innerHTML = t arborize rs = remotes true
   exports.$i.innerHTML = tHint rs.length


Update publishing.iced

Update publishing.iced

--- src/gulp_modules/publishing.iced
+++ src/gulp_modules/publishing.iced
@@ -151,6 +151,6 @@
 (You don't want to download these files directly, there's no point.)
 """,
       draft: false,
-      prerelease: if argv.nightly then true else false,
+      prerelease: if argv.nightly or argv.preview then true else false,
     }


remove commented-out code

remove commented-out code

--- src/core/views/collection.coffee
+++ src/core/views/collection.coffee
@@ -61,9 +61,7 @@

     _call: (method, event, source, callback = (() ->)) ->

-      # callstack = flatstack @
-
-      # first time being called?
+      # first time being called? It's NOW the current state
       if @get("currentState") isnt method
         @set "currentState", method
         @emit method
@@ -101,7 +99,7 @@

         #load, loaded, display, displayed, etc.
         @set event, true
-
+
         @emit event
         callback()
         return


Removed spurious logging

Removed spurious logging

--- src/main/client/controls/controls-directives.coffee
+++ src/main/client/controls/controls-directives.coffee
@@ -10,12 +10,10 @@
         model: '='
         ngDisabled: '='
       link: (scope, iElement, iAttrs) ->
-        console.log "Initializing selectizeTags"

         selectize = undefined

         scope.$watch 'options', (options) ->
-          console.log "Received options", options
           select = jQuery(iElement).selectize
             maxItems: null
             valueField: 'text'
@@ -27,7 +25,6 @@
             persist: false

           selectize = select[0].selectize
-          console.log "Called selectize", selectize

           if scope.ngDisabled
             selectize.disable()
@@ -39,15 +36,12 @@

         ## Tags are a collection
         scope.$watchCollection 'model', (value, old) ->
-          console.log "Model update", value, old
           if selectize
             $timeout () ->
               if value && Array.isArray(value)
                 selectize.clearOptions()
                 for tag in value
-                  console.log "Adding option", {text: tag}
                   selectize.addOption({text: tag})
-                console.log "Setting selectize value", value
                 selectize.setValue(value)
                 initialized = true


Use substring instead of substr

Use substring instead of substr

--- src/app/text-mate-grammar.coffee
+++ src/app/text-mate-grammar.coffee
@@ -408,7 +408,7 @@
       patternScope = @scopeName.replace /\$\d+/, (match) ->
         captureIndex = parseInt(match.substring(1)) * 3
         if captureIndices[captureIndex]?
-          line.substr(captureIndices[captureIndex + 1], captureIndices[captureIndex + 2])
+          line.substring(captureIndices[captureIndex + 1], captureIndices[captureIndex + 2])
         else
           match
       scopes.push(patternScope)


pickMediaDialog start

pickMediaDialog start

--- js/editor.coffee
+++ js/editor.coffee
@@ -108,7 +108,8 @@
    @listen 'italic', -> @wrapSelection '--', '--'
    @listen 'inlineCode', -> @wrapSelection '``', '``'
    @listen 'link', -> @wrapSelection '<<', '>>'
-   @listen 'inlineMedia', -> @wrapSelection '[[', ']]'
+   @listen 'inlineMedia', ->
+    @pickMediaDialog()
    @listen 'superscript', -> @wrapSelection '^^', '^^'
    @listen 'subscript', -> @wrapSelection '__', '__'

@@ -253,6 +254,12 @@

     window.requestAnimationFrame @on.setupEditor

+   pickMediaDialog: ->
+    s = @editor.getSelection()
+    if s.trim() isnt ''
+     return @wrapSelection '[[', ']]'
+
+
   EDITOR = new Editor
   EDITOR.render ->
    Mod.set 'Editor', EDITOR


Make sure the backbone extensions are only loaded once

--- src/tb/lib/backbone-extensions/index.coffee
+++ src/tb/lib/backbone-extensions/index.coffee
@@ -1,12 +1,13 @@
 ti = require 'tb/lib/ti'
+
+load = _.once ->
+  Backbone.$ = ti.$
+  @extendView()
+  extended = true

 module.exports =

-  load: ->
-
-    Backbone.$ = ti.$
-
-    @extendView()
+  load: load

   extendView: ->


Implemented createBlob, createTree, updateRef, ref, createCommit

Fixes #34

--- src/octonode/repo.coffee
+++ src/octonode/repo.coffee
@@ -50,6 +50,13 @@
       return cb(err) if err
       if s isnt 200 then cb(new Error("Repo commits error")) else cb null, b, h

+  # Create a commit
+  # '/repos/pksunkara/hub/git/commits' POST
+  createCommit: (message, tree, parents, cb) ->
+    @client.post "/repos/#{@name}/git/commits", {message: message, parents: parents, tree: tree}, (err, s, b) ->
+      return cb(err) if err
+      if s isnt 201 then cb(new Error("Repo createCommit error")) else cb null, b
+
   # Get the tags for a repository
   # '/repos/pksunkara/hub/tags' GET
   tags: (cb) ->
@@ -192,6 +199,13 @@
     , (err, s, b, h) ->
       return cb(err) if (err)
       if s isnt 200 then cb(new Error("Repo blob error")) else cb null, b, h
+
+  # Create a blob (for a future commit)
+  # '/repos/pksunkara/hub/git/blobs' POST
+  createBlob: (content, encoding, cb) ->
+    @client.post "/repos/#{@name}/git/blobs", {content: content, encoding: encoding}, (err, s, b) ->
+      return cb(err) if err
+      if s isnt 201 then cb(new Error("Repo createBlob error")) else cb null, b

   # Get a git tree
   # '/repos/pksunkara/hub/git/trees/SHA' GET
@@ -205,6 +219,30 @@
       return cb(err) if err
       if s isnt 200 then cb(new Error("Repo tree error")) else cb null, b, h

+  # Create a tree object
+  # '/repos/pksunkara/hub/git/trees' POST
+  createTree: (tree, cbOrBase, cb) ->
+    if !cb? and cbOrBase
+      cb = cbOrBase
+      cbOrBase = null
+    @client.post "/repos/#{@name}/git/trees", {tree: tree, base_tree: cbOrBase}, (err, s, b) ->
+      return cb(err) if err
+      if s isnt 201 then cb(new Error("Repo createTree error")) else cb null, b
+
+  # Get a reference
+  # '/repos/pksunkara/hub/git/refs/REF' GET
+  ref: (ref, cb) ->
+    @client.get "/repos/#{@name}/git/refs/#{ref}", (err, s, b) ->
+      return cb(err) if err
+      if s isnt 200 then cb(new Error("Repo ref error")) else cb null, b
+
+  # Update a reference
+  # '/repos/pksunkara/hub/git/refs/REF' PATCH
+  updateRef: (ref, sha, cb) ->
+    @client.post "/repos/#{@name}/git/refs/#{ref}", {sha: sha}, (err, s, b) ->
+      return cb(err) if err
+      if s isnt 200 then cb(new Error("Repo updateRef error")) else cb null, b
+
   # Delete the repository
   # '/repos/pksunkara/hub' DELETE
   destroy: ->


slightly modified tests from 'evented-component'

--- source/test/suite/lib/node.coffee
+++ source/test/suite/lib/node.coffee
@@ -1,7 +1,7 @@
 should = require 'should'
 Base = require '../../../lib/base'

-module.exports = (Test, name = 'Element') ->
+module.exports = (Test, name = 'Node') ->
   describe name, ->
     it 'should be a class', ->
       Test.should.be.a.Function
@@ -10,3 +10,38 @@

     it 'should be a subclass of Base', ->
       Test::.should.be.an.instanceof Base
+
+    describe '#constructor()', -> it 'should return an instance of Node', ->
+      (new Test).should.be.an.instanceof Test
+
+    describe 'instance', ->
+      it 'should have parent property', -> (new Test).should.have.property 'parent'
+      it 'should have children property', -> (new Test).should.have.property 'children'
+
+      it 'should have appendChild() method', -> (new Test).appendChild.should.be.a.Function
+      it 'should have removeChild() method', -> (new Test).removeChild.should.be.a.Function
+
+      describe '#children', ->
+        it 'should be an empty array', -> (new Test).children.length.should.be.equal 0
+
+      describe '#appendChild(child)', ->
+        it 'should return the node', -> (test = new Test).appendChild({}).should.be.equal test
+
+        it 'should append the child to children', ->
+          (parent = new Test).appendChild(child = new Test).children[0].should.be.equal child
+
+        it 'should only add an instances of Test (or any subclass)', ->
+          (parent = new Test).appendChild({}).children.length.should.be.equal 0
+
+        it 'should not add duplicates', ->
+          (parent = new Test).appendChild(child = new Test).appendChild(child).children.length.should.be.equal 1
+
+        it 'should remove the child from its parent', ->
+          (parent = new Test).appendChild(child = new Test).children.length.should.be.equal 1
+          (new Test).appendChild(child).children[0].should.be.equal child
+          parent.children.length.should.be.equal 0
+
+      describe '#removeChild(child)', ->
+        it 'should return the node', -> (test = new Test).removeChild({}).should.be.equal test
+        it 'should remove the child', ->
+          (new Test).appendChild(child = new Test).removeChild(child).children.length.should.be.equal 0


Widgets: Add and remove edit-mode class when form is in edit mode.

--- client/Dashboard/views/customviews/customviewsdashboardview.coffee
+++ client/Dashboard/views/customviews/customviewsdashboardview.coffee
@@ -41,6 +41,7 @@

   bindEventHandlers: ->
     @on "NewViewAdded", =>
+      @unsetClass "edit-mode"
       @addNewView.destroy()
       @addNewButton.show()
       @reloadViews()
@@ -54,6 +55,7 @@
       @addNew viewData

     @on "AddingNewViewCancelled", =>
+      @unsetClass "edit-mode"
       @reloadViews()
       @addNewButton.show()

@@ -64,6 +66,7 @@

   addNew: (data) ->
     @hideViews()
+    @setClass "edit-mode"
     appManager = KD.singleton "appManager"
     appManager.require "Teamwork", (app) =>
       config     =


registering select field

registering select field

--- app/assets/javascripts/luca/components/fields/select_field.coffee
+++ app/assets/javascripts/luca/components/fields/select_field.coffee
@@ -154,4 +154,6 @@
         @getInputElement().append( option )

     @trigger "after:populate:options", @
-    @setValue( @currentValue )+    @setValue( @currentValue )
+
+selectField.register()


prevent link container flicker before css is loaded

--- src/modules/link-tooltip.coffee
+++ src/modules/link-tooltip.coffee
@@ -102,8 +102,10 @@
     '#link-tooltip.editing .url'    : { 'display': 'none' }
     '#link-tooltip.editing .change' : { 'display': 'none' }
   )
-  @editor.renderer.addContainer(@tooltip)
-
+  @editor.renderer.runWhenLoaded( =>
+    @editor.renderer.addContainer(@tooltip)
+  )
+
 showTooptip = (target, subjectDist = 5) ->
   tooltip = @tooltip.getBoundingClientRect()
   tooltipHeight = tooltip.bottom - tooltip.top


cosmetics

cosmetics

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -274,7 +274,7 @@
   if MailButler.isEnabled()
     # Get a lock for the current user
     lock = LockService.getPrivateLock()
-    if lock.tryLock(10000)
+    if lock.tryLock 10000

       # wait 10 seconds at most
       Logger.log 'We have the lock...'


Clear dialog contents on close.

Clear dialog contents on close.

No need to leave the material there.

--- kirppu/static_src/js/checkout/dialog.coffee
+++ kirppu/static_src/js/checkout/dialog.coffee
@@ -12,6 +12,11 @@

     @btnPositive = null
     @btnNegative = null
+    @container.on("hidden.bs.modal", () =>
+      @title.empty()
+      @body.empty()
+      @buttons.empty()
+    )

   # Add positive button to the dialog.
   # @param clazz [optional] Initial btn-class to set to the button.


<controller> Defining the routes.

<controller> Defining the routes.

--- server/controllers/routes.coffee
+++ server/controllers/routes.coffee
@@ -1,8 +1,61 @@
-# See documentation on https://github.com/frankrousseau/americano#routes
+###
+# =================
+# CONTROLLER - Routes
+# =================
+# Routes to call the controllers.
+###

-index = require './index'
+sensor			= require './sensors'
+actuator		= require './actuator'
+measure			= require './measure'
+rule			= require './rule'
+sensorRule		= require './sensorRule'
+actuatorRule	= require './actuatorRule'

 module.exports =
-    'foo':
-        get: index.main

+    # Fetch on params
+    'sensorId': 	param: sensor.fetch
+    'actuatorId': 	param: actuator.fetch
+    'ruleId': 		param: rule.fetch
+    'measureId': 	param: measure.fetch
+
+    'sensors/?':
+        get: sensor.all
+        post: sensor.create
+
+    'sensors/:sensorId/?':
+        get: sensor.read
+        put: sensor.update
+        delete: sensor.delete
+
+    'sensors/:sensorId/measures?':
+        # @todo Get by Sensor + Date (opt): get: sensor.getMeasuresByDate
+        post: sensor.createMeasure
+
+    'actuators/?':
+        get: actuator.all
+        post: actuator.create
+
+    'actuators/:actuatorId/?':
+        get: actuator.read
+        put: actuator.update
+        delete: actuator.delete
+
+    'rules/?':
+        get: rule.all
+        post: rule.create
+
+    'rules/:ruleId/?':
+        get: rule.read
+        put: rule.update
+        delete: rule.delete
+
+    'rules/:ruleId/sensorRules?':
+        # @todo Get by Rule: get: sensor.getSensorRules
+        post: rule.createSensorRule
+
+
+    'rules/:ruleId/actuatorRules?':
+        # @todo Get by Rule: get: sensor.getActuatorRules
+        post: rule.createActuatorRule


Pass ArrayPort information to dataflow

Pass ArrayPort information to dataflow

--- src/plugins/nofloLibrary.coffee
+++ src/plugins/nofloLibrary.coffee
@@ -19,7 +19,12 @@

     runtime.on 'component', (message) =>
       return unless runtime is @runtime
-      @registerComponent message.payload
+      component = message.payload
+      for port in component.inPorts
+        port.multiple = if port.array then true else false
+      for port in component.outPorts
+        port.multiple = if port.array then true else false
+      @registerComponent component

     # Load components once we have a connection
     runtime.on 'connected', =>


find teams by id or slug

find teams by id or slug

--- models/models.coffee
+++ models/models.coffee
@@ -226,6 +226,9 @@

 Team::toParam = -> @slug
 Team.fromParam = (id, options, fn) ->
-  @first { slug: id }, options, fn
+  if id.length == 24
+    @first { '$or': [ { slug: id }, Mongo.queryify(id) ] }, options, fn
+  else
+    @first { slug: id }, options, fn

 _.extend exports, nko


Constrain the cache size by removing the oldest caches

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -55,21 +55,27 @@

 cacheCurrentPage = ->
   pageCache[currentState.url] =
-    body:      document.body,
-    title:     document.title,
-    positionY: window.pageYOffset,
-    positionX: window.pageXOffset
-
-  #constrainPageCacheTo cacheSize
+    body:             document.body,
+    title:            document.title,
+    positionY:        window.pageYOffset,
+    positionX:        window.pageXOffset,
+    cachedAt:         new Date().getTime()
+
+  constrainPageCacheTo cacheSize

 pagesCached = (size = cacheSize) ->
   cacheSize = parseInt(size) if /^[\d]+$/.test size

 constrainPageCacheTo = (limit) ->
-  for own key, value of pageCache when key <= currentState.position - limit
+  pageCacheKeys = Object.keys pageCache
+
+  cacheTimesRecentFirst = pageCacheKeys.map (url) ->
+    pageCache[url].cachedAt
+  .sort (a, b) -> b - a
+
+  for key in pageCacheKeys when pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]
     triggerEvent 'page:expire', pageCache[key]
-    pageCache[key] = null
-  return
+    delete pageCache[key]

 changePage = (title, body, csrfToken, runScripts) ->
   document.title = title


comment on line count

comment on line count

--- chuck.coffee
+++ chuck.coffee
@@ -105,7 +105,7 @@
   metrics.methodCount = metrics.methods.length
   metrics.propertyCount = (m for m in cls.body when m.member is 'property').length
   metrics.innerClassCount = (m for m in cls.body when m.member is 'inner_class').length
-  metrics.lines = cls.position.last_line+1
+  metrics.lines = cls.position.last_line+1 # due to a bug in the ascent parser position information
   metrics.linesPerMethod = metrics.lines / metrics.methods.length
   metrics.complexity = sum(m.complexity for m in metrics.methods)
   metrics.complexityPerMethod = metrics.complexity / metrics.methods.length


added animation helper

added animation helper

--- src/scene/objects/entity.coffee
+++ src/scene/objects/entity.coffee
@@ -63,6 +63,10 @@
   is: (tag) ->
     @animation.tag == tag

+  # Public: Returns the name of the active animation.
+  active: () ->
+    @animation.tag
+
   # Public: Adds an animation.
   #
   # tag - name of the animation


Prevent infinite loop on sandbox share

Prevent infinite loop on sandbox share

--- src/client/embeds/sandbox.coffee
+++ src/client/embeds/sandbox.coffee
@@ -45,7 +45,7 @@
     # Enrich UTM Tags Defaults
     opts.tags = assign({}, opts.tags, {utm_campaign:@ship.name})

-    @hull.share(opts,event)
+    Hull.share(opts,event)

   getShipClassName : => @shipClassName

@@ -78,6 +78,7 @@
     # sandbox.addElement(insertion.el)

     sandbox.autoSizeInterval = null;
+    mainShare = @share
     hull = assign({}, @hull, {
       onEmbed : (args...)=>
         callback = args.shift() while (args.length>0 && !_.isFunction(callback))
@@ -87,7 +88,7 @@
       getDocument : ()=> doc
       share : (opts, event={})->
         event = assign(event, {target: iframe})
-        @share(opts,event);
+        mainShare(opts,event);
       findUrl : ()-> findUrl(iframe)
       setSize : (size={})=>
         style = assign({},size)


Comments

Comments

--- src/scripts/airline_tycoon.cjsx
+++ src/scripts/airline_tycoon.cjsx
@@ -17,12 +17,12 @@
   color: grey

 planes = [
-  {name: 'Plane1', flights: 0, location: 'DUB', owner: player1},
-  {name: 'Plane2', flights: 0, location: 'DUB', owner: player1},
-  {name: 'Plane3', flights: 0, location: 'DUB', owner: player1},
-  {name: 'Plane4', flights: 0, location: 'NYC', owner: player2},
-  {name: 'Plane5', flights: 0, location: 'NYC', owner: player2},
-  {name: 'Plane6', flights: 0, location: 'NYC', owner: player2}
+  {name: 'Plane1', flights_flown: 0, location: 'DUB', owner: player1},
+  {name: 'Plane2', flights_flown: 0, location: 'DUB', owner: player1},
+  {name: 'Plane3', flights_flown: 0, location: 'DUB', owner: player1},
+  {name: 'Plane4', flights_flown: 0, location: 'NYC', owner: player2},
+  {name: 'Plane5', flights_flown: 0, location: 'NYC', owner: player2},
+  {name: 'Plane6', flights_flown: 0, location: 'NYC', owner: player2}
 ]

 planes_at = (location) ->
@@ -35,7 +35,7 @@
   {key: 'DUB', name: 'DUB', left: 770, top:  380, owner: player1 }
 ]

-# Some data structure to store flights as they move around...
+# Some "virtual" places flight can live when they're moving between airports
 routes = [
   { key: 'NYC->LHR', name: 'NYC->LHR', start: 'NYC', end: 'LHR', x: 450, y: 240 },
   { key: 'LHR->NYC', name: 'LHR->NYC', start: 'LHR', end: 'NYC', x: 472, y: 326 },


Allowed Chess to be channel/room independant

The previous script only allowed one game to be run per bot. This simply stores the chess games in a store relative to the channel name, allowing multiple games to be run at one time.

--- src/scripts/chess.coffee
+++ src/scripts/chess.coffee
@@ -20,12 +20,12 @@

 module.exports = (robot) ->
   robot.respond /chess me$/i, (msg) ->
-    robot.brain.data.chess = Chess.create()
-    boardToFen robot.brain.data.chess.getStatus(), (status, fen) ->
+    robot.brain.data.chess[msg.message.room] = Chess.create()
+    boardToFen robot.brain.data.chess[msg.message.room].getStatus(), (status, fen) ->
       msg.send 'http://webchess.freehostia.com/diag/chessdiag.php?fen=' + encodeURIComponent(fen) + '&size=large&coord=yes&cap=yes&stm=yes&fb=no&theme=classic&format=auto&color1=E3CEAA&color2=635147&color3=000000&.png'
   robot.respond /chess status/i, (msg) ->
     try
-      boardToFen robot.brain.data.chess.getStatus(), (status, fen) ->
+      boardToFen robot.brain.data.chess[msg.message.room].getStatus(), (status, fen) ->
         if status
           msg.send status
         msg.send 'http://webchess.freehostia.com/diag/chessdiag.php?fen=' + encodeURIComponent(fen) + '&size=large&coord=yes&cap=yes&stm=yes&fb=no&theme=classic&format=auto&color1=E3CEAA&color2=635147&color3=000000&.png'
@@ -34,8 +34,8 @@

   robot.respond /chess move (.*)/i, (msg) ->
     try
-      robot.brain.data.chess.move msg.match[1]
-      boardToFen robot.brain.data.chess.getStatus(), (status, fen) ->
+      robot.brain.data.chess[msg.message.room].move msg.match[1]
+      boardToFen robot.brain.data.chess[msg.message.room].getStatus(), (status, fen) ->
        if status
           msg.send status
         msg.send 'http://webchess.freehostia.com/diag/chessdiag.php?fen=' + encodeURIComponent(fen) + '&size=large&coord=yes&cap=yes&stm=yes&fb=no&theme=classic&format=auto&color1=E3CEAA&color2=635147&color3=000000&.png'


Fix elastic.io checks on passed message.

--- src/coffee/stockxmlimport.coffee
+++ src/coffee/stockxmlimport.coffee
@@ -9,7 +9,7 @@

   elasticio: (msg, cfg, cb, snapshot) ->
     console.log 'elasticio'
-    if msg.attachments
+    if _.size(msg.attachments) > 0
       console.log 'elasticio - attachments'
       for attachment of msg.attachments
         continue if not attachment.match /xml$/i
@@ -17,7 +17,7 @@
         continue if not content
         xmlString = new Buffer(content, 'base64').toString()
         @run xmlString, cb
-    else if msg.body
+    else if _.size(msg.body) > 0
       console.log 'elasticio - body'
       # TODO: As we get only one entry here, we should query for the existing one and not
       # get the whole inventory


Move all auto-indent specs to the auto-indent describe block

--- spec/app/edit-session-spec.coffee
+++ spec/app/edit-session-spec.coffee
@@ -1011,53 +1011,6 @@
           editSession.insertText('holy cow')
           expect(editSession.lineForScreenRow(2).fold).toBeUndefined()

-      describe "when autoIndentNewlines is enabled", ->
-        describe "when the newline is added at the end of a line that starts a new level of indentation", ->
-          it "auto indents the new line to one additional level of indentation beyond the preceding line", ->
-            editSession.setCursorBufferPosition([1, Infinity])
-            editSession.insertText('\n', autoIndentNewlines: true)
-            expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
-
-        describe "when the newline is inserted on a normal line", ->
-          it "auto-indents the new line to the same level of indentation as the preceding line", ->
-            editSession.setCursorBufferPosition([5, 13])
-            editSession.insertText('\n', autoIndentNewlines: true)
-            expect(editSession.indentationForBufferRow(6)).toBe editSession.indentationForBufferRow(5)
-
-      describe "when autoDecreaseIndent is enabled", ->
-        describe "when the current line matches an auto-outdent pattern", ->
-          describe "when the preceding line matches an auto-indent pattern", ->
-            it "auto-decreases the indentation of the line to match that of the preceding line", ->
-              editSession.setCursorBufferPosition([1, Infinity])
-              editSession.insertText('\n    ')
-              expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
-              editSession.insertText('}', autoDecreaseIndent: true)
-              expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1)
-
-          describe "when the preceding line does not match an auto-indent pattern", ->
-            describe "when the inserted text does not match an auto-decrease pattern", ->
-              it "does not auto-decrease the indentation", ->
-                editSession.setCursorBufferPosition([12, 0])
-                editSession.insertText('  ')
-                expect(editSession.lineForBufferRow(12)).toBe '  };'
-                editSession.insertText('\t\t', autoDecreaseIndent: true)
-                expect(editSession.lineForBufferRow(12)).toBe '  \t\t};'
-
-            describe "when the inserted text matches an auto-decrease pattern", ->
-              it "auto-decreases the indentation of the line to be one level below that of the preceding line", ->
-                editSession.setCursorBufferPosition([3, Infinity])
-                editSession.insertText('\n    ')
-                expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3)
-                editSession.insertText('}', autoDecreaseIndent: true)
-                expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1
-
-        describe "when the current line does not match an auto-outdent pattern", ->
-          it "leaves the line unchanged", ->
-            editSession.setCursorBufferPosition([2, 4])
-            expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
-            editSession.insertText('foo', autoIndent: true)
-            expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
-
       xdescribe "when the `normalizeIndent` option is true", ->
         describe "when the inserted text contains no newlines", ->
           it "does not adjust the indentation level of the text", ->
@@ -2399,28 +2352,54 @@
           editSession.indent()
           expect(editSession.lineForBufferRow(2)).toBe "   "

-      it "auto-indents newlines if editor.autoIndent is true", ->
-        config.set("editor.autoIndent", undefined)
-        editSession.setCursorBufferPosition([1, 30])
-        editSession.insertText("\n")
-        expect(editSession.lineForBufferRow(2)).toBe "    "
-
-      it "does not auto-indent newlines if editor.autoIndent is false", ->
-        config.set("editor.autoIndent", false)
-        editSession.setCursorBufferPosition([1, 30])
-        editSession.insertText("\n")
-        expect(editSession.lineForBufferRow(2)).toBe ""
-
-      it "auto-indents selection when autoIndent is called", ->
-        editSession.setCursorBufferPosition([2, 0])
-        editSession.insertText("    0\n  2\n4\n")
-
-        editSession.setSelectedBufferRange([[2, 0], [4, 0]])
-        editSession.autoIndentSelectedRows()
-
-        expect(editSession.lineForBufferRow(2)).toBe "    0"
-        expect(editSession.lineForBufferRow(3)).toBe "    2"
-        expect(editSession.lineForBufferRow(4)).toBe "4"
+      describe "when a newline is added", ->
+        describe "when editor.autoIndent is true", ->
+          beforeEach ->
+            config.set("editor.autoIndent", true)
+
+          describe "when the line above the newline adds a new level of indentation", ->
+            it "auto indents the newline to one additional level of indentation beyond the preceding line", ->
+              editSession.setCursorBufferPosition([1, Infinity])
+              editSession.insertText('\n')
+              expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
+
+          describe "when the line above the newline adds a new level of indentation", ->
+            it "auto-indents the new line to the same level of indentation as the preceding line", ->
+              editSession.setCursorBufferPosition([5, 13])
+              editSession.insertText('\n')
+              expect(editSession.indentationForBufferRow(6)).toBe editSession.indentationForBufferRow(5)
+
+      describe "when inserted text matches a decrease indent pattern", ->
+        describe "when the preceding line matches an increase indent pattern", ->
+          it "decreases the indentation of the line to match that of the preceding line", ->
+            editSession.setCursorBufferPosition([1, Infinity])
+            editSession.insertText('\n    ')
+            expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
+            editSession.insertText('}', autoDecreaseIndent: true)
+            expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1)
+
+        describe "when the preceding line doesn't match an increase indent pattern", ->
+          it "decreases the indentation of the line to be one level below that of the preceding line", ->
+            editSession.setCursorBufferPosition([3, Infinity])
+            editSession.insertText('\n    ')
+            expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3)
+            editSession.insertText('}', autoDecreaseIndent: true)
+            expect(editSession.indentationForBufferRow(4)).toBe editSession.indentationForBufferRow(3) - 1
+
+      describe "when inserted text does not match a decrease indent pattern", ->
+        it "does not  the indentation", ->
+          editSession.setCursorBufferPosition([12, 0])
+          editSession.insertText('  ')
+          expect(editSession.lineForBufferRow(12)).toBe '  };'
+          editSession.insertText('\t\t', autoDecreaseIndent: true)
+          expect(editSession.lineForBufferRow(12)).toBe '  \t\t};'
+
+      describe "when the current line does not match a decrease indent pattern", ->
+        it "leaves the line unchanged", ->
+          editSession.setCursorBufferPosition([2, 4])
+          expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1
+          editSession.insertText('foo', autoIndent: true)
+          expect(editSession.indentationForBufferRow(2)).toBe editSession.indentationForBufferRow(1) + 1

     describe "editor.autoIndentOnPaste", ->
       describe "when the text contains multiple lines", ->
@@ -2466,6 +2445,17 @@
           config.set("editor.autoIndentOnPaste", true)
           editSession.pasteText()
           expect(editSession.lineForBufferRow(10)).toBe "  var number"
+
+    it "autoIndentSelectedRows auto-indents the selection", ->
+      editSession.setCursorBufferPosition([2, 0])
+      editSession.insertText("function() {\ninside=true\n}\n  i=1\n")
+      editSession.getSelection().setBufferRange([[2,0], [6,0]])
+      editSession.autoIndentSelectedRows()
+
+      expect(editSession.lineForBufferRow(2)).toBe "    function() {"
+      expect(editSession.lineForBufferRow(3)).toBe "      inside=true"
+      expect(editSession.lineForBufferRow(4)).toBe "    }"
+      expect(editSession.lineForBufferRow(5)).toBe "    i=1"

   describe ".autoDecreaseIndentForRow()", ->
       it "doesn't outdent the first and only row", ->


adding the execute and parse command, making this more of an interface

--- src/scripts/drush.coffee
+++ src/scripts/drush.coffee
@@ -7,38 +7,61 @@
 util = require("util")
 spawn = require("child_process").spawn

-drush_ui = ->
+drush_interface = ->
   site_aliases = []
   allowed_commands = ["sa", "cc", "pml", "unif", "pmi", "ws", "vget", "rq"]

+  # Our allowed commands
+  commands =
+    drush_sa: (msg) ->
+      msg.send "Aliases we have in memory, if this list is empty or is unexpected update aliases."
+      msg.send site_aliases.join("\n")
+
+  # parsing the user input after "drush "
+  parse_command = (user_command) ->
+    args = user_command.split(' ')
+    command = "drush_" + args.shift()
+    if typeof commands[command] is "function"
+      return (
+        cmnd: command
+        extra_args: args
+      )
+      command
+    else
+      `undefined`
+
+  # Begin our public facing methods
   update_aliases: (msg) ->
     output = ''
+    raw_aliases = ''
     fetch_aliases = spawn("drush", ["sa"])
     fetch_aliases.stdout.on "data", (data) ->
-      output = output + data
+      raw_aliases += data
     fetch_aliases.stderr.on "data", (data) ->
       output = "Update experienced an error: " + data
     fetch_aliases.on "exit", (code) ->
       if code is 0
-        site_aliases = output.split('\n')
-        msg.send "aliases updated first element: " + site_aliases[1]
-      else
+        site_aliases = raw_aliases.split('\n')
+        output = "drush aliases updated"
+      unless msg is `undefined`
         msg.send output

-  show_stored_aliases: (msg) ->
-    msg.send "Aliases we have in memory, if this list is empty or is unexpected update aliases."
-    msg.send site_aliases.join("\n")
+  # The main method, fire this when we recieve a "drush " command.
+  execute: (msg) ->
+    msg.send "we got args: " + msg.match[1]
+    command = parse_command(msg.match[1])
+    unless command is `undefined`
+      commands[command.cmnd](msg)
+    else
+      msg.send "'drush " + msg.match[1] + "' is and invalid command. Please try again."

-drush_instance = drush_ui()
+drush = drush_interface()
+drush.update_aliases()

 module.exports = (robot) ->
-  robot.respond /drush update-aliases$/i, (msg) ->
+  robot.respond /drush (.*)$/i, (msg) ->
     msg.send "drush fired!"
-    drush_instance.update_aliases(msg)
-
-  robot.respond /drush sa$/i, (msg) ->
-    msg.send "drush fired!"
-    drush_instance.show_stored_aliases(msg)
+    drush.execute(msg)

 #module.exports = (robot) ->
 #  robot.respond /drush sa$/i, (msg) ->


Added hound override flag to hound to force hounding on presence change

--- src/scripts/hound.coffee
+++ src/scripts/hound.coffee
@@ -25,7 +25,7 @@
   #   If the user is logged in, the DM should say:
   #     Don’t forget to check out~

-  hound = (slackuser, channel) ->
+  hound = (slackuser, channel, forceHound=false) ->
     if not channel.private
       channel.private = !!channel.is_im or !!channel.is_group
     if not Organization.ready()
@@ -65,7 +65,7 @@
         user.shouldHound = true
       return

-    if timeSinceLastMessage >= 3 or timeSinceLastPunch >= 3
+    if timeSinceLastMessage >= 3 or timeSinceLastPunch >= 3 or forceHound
       if lastPunch.mode is 'in'
         user.directMessage "Don't forget to check out~", Logger
         user.shouldHound = false
@@ -92,7 +92,7 @@
       Logger.log "Don\'t run scheduled reset, Organization isn\'t ready yet"
       return
     for user in Organization.users
-      hound { name: user.slack}, { private: null , name: ''}
+      hound { name: user.slack}, { private: null , name: ''}, true

   # Every morning, reset hound status for each users


Fix syntax in error message

Fix syntax in error message

--- src/marbles/collection.js.coffee
+++ src/marbles/collection.js.coffee
@@ -85,7 +85,7 @@
     @trigger 'detach', @

   fetch: (params = {}, options = {}) =>
-    throw new Error("You need to define #{@constructor.name}::fetch(params, options)!")
+    throw new Error("You need to define #{@constructor.name}.prototype.fetch(params, options)!")

   empty: =>
     @model_ids = []


Permitir datas de 1 a 12

Permitir datas de 1 a 12

Permitir datas de 1 a 12

--- src/simditor-date-button.coffee
+++ src/simditor-date-button.coffee
@@ -32,7 +32,7 @@
   _getDate: ->
     now = new Date()
     day = now.getDate()
-    month = now.getMonth()
+    month = now.getMonth() + 1
     year = now.getFullYear()

     parse = (n) -> if n > 9 then n else "0#{n}"


hub/email: basically, the welcome email content

--- src/smc-hub/email.coffee
+++ src/smc-hub/email.coffee
@@ -40,7 +40,7 @@
 misc         = require('smc-util/misc')
 {defaults, required} = misc

-{SENDGRID_TEMPLATE_ID, SENDGRID_ASM_NEWSLETTER, COMPANY_NAME, COMPANY_EMAIL, DOMAIN_NAME, SITE_NAME} = require('smc-util/theme')
+{SENDGRID_TEMPLATE_ID, SENDGRID_ASM_NEWSLETTER, COMPANY_NAME, COMPANY_EMAIL, DOMAIN_NAME, SITE_NAME, DNS, HELP_EMAIL} = require('smc-util/theme')

 email_server = undefined

@@ -229,6 +229,106 @@
     )

+# beware, this needs to be HTML which is compatible with email-clients!
+welcome_email_html = (token_url) -> """
+
Welcome to #{SITE_NAME}

+
+

+#{SITE_NAME} is sophisticated web service for collaborative computation.
+

+
+

+You receive this email because an account with your email address was created.
+This was either initiated by you, a friend or colleague invited you, or you're a student as part of a course.
+

+
+

+
+Please click here to verify your email address!
+If this link does not work, please copy/paste this URL into a new browser tab and open the link:
+
+

+
+

+#{token_url}
+

+
+

+
+
Explore #{SITE_NAME}

+

+Your work on #{SITE_NAME} happens inside private projects.
+They form your personal workspaces containing your files, computational worksheets, and data.
+You run your computations through the web interface, via interactive worksheets and notebooks, or by executing a program in a terminal.
+#{SITE_NAME} supports online editing of
+    Jupyter Notebooks,
+    Sage Worksheets,
+    Latex files, etc.
+

+
+
Software:
+

+
Mathematical calculation:
+    SageMath,
+    SymPy, etc.
+

+
Statistical analysis:
+    R project,
+    Pandas,
+    statsmodels,
+    scikit-learn,
+    NLTK, etc.
+

+
Various other computation:
+    Tensorflow,
+    Octave,
+    Julia, etc.
+

+

+
+

+Visit our Software overview page for more details!
+

+
+

+Collaboration:
+You can invite collaborators to work with you inside a project.
+Like you, they can edit the files in that project.
+Edits are visible in real time for everyone online.
+You can share your thoughts in a side chat next to each document.
+

+
+
More information: how to get from 0 to 100%!

+
+

+

+    #{SITE_NAME} Wiki: the entry-point to learn more about all the details.
+

+

+    Working with SageMath Worksheets
+

+

+    Subscriptions: make hosting more robust and increase project quotas
+

+

+    Sophisticated tools for teaching a class.
+

+

+    Troubleshooting connectivity issues
+

+

+    Common mathematical syntax errors: look into this if you are new to working with a programming language!
+

+

+
+

+Questions?
+In case of problems, concerns why you received this email, or other questions please contact:
+#{HELP_EMAIL}.
+

+
+"""
+
 exports.welcome_email = (opts) ->
     opts = defaults opts,
         to           : required
@@ -240,20 +340,11 @@
     token_query = encodeURI("email=#{encodeURIComponent(opts.to)}&token=#{opts.token}")
     endpoint    = os_path.join('/', base_url, 'auth/verify')
     token_url   = "#{DOMAIN_NAME}#{endpoint}?#{token_query}"
-    token_html  = """
-

-    Please click here: #{token_url} to verify your email address!
-

-    """
-
-    body = """
-
Welcome to #{SITE_NAME}

-
-    #{token_html}
-    """
+
+    body = welcome_email_html(token_url)

     send_email
-        subject      : "Welcome to #{SITE_NAME}"
+        subject      : "Welcome to #{SITE_NAME} - #{DNS}"
         body         : body
         fromname     : COMPANY_NAME
         from         : COMPANY_EMAIL


fix(menu): Explicitly remove the active item when closing the menu

--- src/components/menu/index.coffee
+++ src/components/menu/index.coffee
@@ -265,6 +265,10 @@

     @dropdown = new Dropdown(@selector, dropdownContainer, @options.dropdownOptions)

+    @dropdown.on 'hideend', ->
+      self.cursorPos = -1
+      dropdownContainer.selectAll('.hx-menu-item').classed('hx-menu-active', false);
+
     @dropdown.on 'showend', =>
       if @dropdown._.dropdown?
         node = @dropdown._.dropdown.node()


reading/hw cell sort

reading/hw cell sort

--- src/components/scores/index.cjsx
+++ src/components/scores/index.cjsx
@@ -104,8 +104,16 @@
         record = d.data[index]
         return 0 unless record
         switch record.type
-          when 'reading'  then record.status
-          when 'homework' then record.correct_exercise_count or 0
+          when 'reading'  then @percent(record.completed_step_count, record.step_count) or 0
+          when 'homework'
+            switch sort.dataType
+              when 'score'
+                if displayAs is 'number'
+                  record.correct_exercise_count or 0
+                else
+                  @percent(record.correct_exercise_count, record.exercise_count) or 0
+              when 'completed'
+                @percent(record.completed_exercise_count, record.exercise_count) or 0
           when 'concept_coach'
             switch sort.dataType
               when 'score'


webapp/editor/md: multiline selection for lists and enumerations; styling of the md example table -- #1234

--- src/smc-webapp/buttonbar.coffee
+++ src/smc-webapp/buttonbar.coffee
@@ -325,12 +325,16 @@
                 right : '~~'
         insertunorderedlist :
             wrap :
-                left  : "\n - "
-                right : "\n"
+                left  : " -"
+                right : ''
+                multi : true
+                space : true
         insertorderedlist :
             wrap :
-                left  : "\n 1. "
-                right : "\n"
+                left  : "1."
+                right : ''
+                multi : true
+                space : true
         format_heading_1 :  # FUTURE -- define via for loop below
             strip : ['format_heading_2','format_heading_3','format_heading_4']
             wrap :
@@ -367,10 +371,10 @@
             wrap:
                 left : """
                        | Left-Aligned  | Center Aligned  | Right Aligned |
-                       | :------------ |:---------------:| -----:|
-                       | col 3 is      | some wordy text | 1600 |
-                       | col 2 is      | centered        |  12 |
-                       | zebra stripes | and math       |  $\\pi^3$ |
+                       | :------------ |:---------------:| -------------:|
+                       | col 3 is      | some wordy text |          1600 |
+                       | col 2 is      |    centered     |            12 |
+                       | zebra stripes |    and math     |      $\\pi^3$ |
                        """
                 right : ""


Chat: Add send button

Chat: Add send button

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -503,50 +503,43 @@
     getInitialState: ->
         input : ''

+    mark_as_read: ->
+        @props.redux.getActions('file_use').mark_file(@props.project_id, @props.path, 'read')
+
     keydown : (e) ->
         # TODO: Add timeout component to is_typing
         if e.keyCode==27 # ESC
             e.preventDefault()
             @clear_input()
-        else if e.keyCode==13 and not e.shiftKey # 13: enter key
-            @scroll_to_bottom()
-            e.preventDefault()
-            mesg = @refs.input.getValue()
-            # block sending empty messages
-            if mesg.length? and mesg.trim().length >= 1
-                @props.actions.send_chat(mesg)
-                @clear_input()
+        else if e.keyCode==13 and e.shiftKey # 13: enter key
+            @send_chat(e)
         else if e.keyCode==38 and @refs.input.getValue() == ''
+            # Up arrow on an empty input
             @props.actions.set_to_last_input()
+
+    send_chat: (e) ->
+        @scroll_to_bottom()
+        e.preventDefault()
+        mesg = @refs.input.getValue()
+        # block sending empty messages
+        if mesg.length? and mesg.trim().length >= 1
+            @props.actions.send_chat(mesg)
+            @clear_input()

     clear_input: ->
         @props.actions.set_input('')

-    render_input: ->
+    render_bottom_tip: ->
         tip =
-            You may enter (Github flavored) markdown here and include Latex mathematics in $ signs.  In particular, use # for headings, > for block quotes, *'s for italic text, **'s for bold text, - at the beginning of a line for lists, back ticks ` for code, and URL's will automatically become links.   Press shift+enter for a newline without submitting your chat. Double click to edit past chats.
+            You may enter (Github flavored) markdown here and include Latex mathematics in $ signs.  In particular, use # for headings, > for block quotes, *'s for italic text, **'s for bold text, - at the beginning of a line for lists, back ticks ` for code, and URL's will automatically become links.   Press shift+enter to send your chat. Double click to edit past chats.

-        return

-            <Input
-                autoFocus
-                rows      = 4
-                type      = 'textarea'
-                ref       = 'input'
-                onKeyDown = {@keydown}
-                value     = {@props.input}
-                onClick   = {=>@props.redux.getActions('file_use').mark_file(@props.project_id, @props.path, 'read')}
-                onChange  = {(value)=>@props.actions.set_input(@refs.input.getValue())}
-                />
-

-                <Tip title='Use Markdown' tip={tip}>
-                    Shift+Enter for newline.
-                    Double click to edit past chats.
-                    Format using Markdown.
-                    Emoticons: {misc.emoticons}.
-                </Tip>
-

-

+        <Tip title='Use Markdown' tip={tip}>
+            Shift+Enter to send your message.
+            Double click chat bubbles to edit them.
+            Format using Markdown.
+            Emoticons: {misc.emoticons}.
+        </Tip>

     chat_log_style:
         overflowY    : "auto"
@@ -557,9 +550,8 @@
         paddingRight : "10px"

     chat_input_style:
-        height       : "0vh"
         margin       : "0"
-        padding      : "0"
+        padding      : "4px 7px 4px 7px"
         marginTop    : "5px"

     is_at_bottom: ->
@@ -627,7 +619,7 @@
                               user_map    = {@props.user_map} />

                 </Col>
-                <Col xs={4}>
+                <Col xs={4} style={padding:'2px'}>
                     <ButtonGroup style={float:'right'}>
                         <Button onClick={@show_timetravel} bsStyle='info'>
                             <Icon name='history'/> TimeTravel
@@ -639,7 +631,7 @@
                 </Col>
             </Row>
             <Row>
-                <Col md={12}>
+                <Col md={12} style={padding:'0px 2px 0px 2px'}>
                     <Panel style={@chat_log_style} ref='log_container' onScroll={@on_scroll} >
                         <ChatLog
                             messages     = {@props.messages}
@@ -653,10 +645,27 @@
                 </Col>
             </Row>
             <Row>
+                <Col md={11} style={padding:'0px 2px 0px 2px'}>
+                    <Input
+                        autoFocus   = {true}
+                        rows        = 4
+                        type        = 'textarea'
+                        ref         = 'input'
+                        onKeyDown   = {@keydown}
+                        value       = {@props.input}
+                        placeholder = {'Type a message...'}
+                        onClick     = {@mark_as_read}
+                        onChange    = {(value)=>@props.actions.set_input(@refs.input.getValue())}
+                        style       = {@chat_input_style}
+                        />
+                </Col>
+                <Col md={1} style={height:'98.6px', padding:'0px 2px 0px 2px'}>
+                    <Button onClick={@send_chat} bsStyle='primary' style={height:'90%', width:'100%', marginTop:'5px'}>Send</Button>
+                </Col>
+            </Row>
+            <Row>
                 <Col md={12}>
-

-                        {@render_input()}
-

+                    {@render_bottom_tip()}
                 </Col>
             </Row>
         </Grid>


make some chatroom code more defensive to avoid traceback in edge case

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -149,6 +149,9 @@

     _syncdb_change: (changes) =>
         messages_before = messages = @store.get('messages')
+        if not messages?
+            # Messages need not be defined when changes appear in case of problems or race.
+            return
         for x in changes
             if x.insert
                 # Assumes all fields to be provided in x.insert


#17 walk and watch method added, when add folder

--- src/controller.datafluxes.coffee
+++ src/controller.datafluxes.coffee
@@ -40,6 +40,8 @@
         fluxFolder = @path.join srcFolder, @conf.settings.datafluxDefaultDir.value
       new_df = new Dataflux(srcFolder, fluxFolder)
       new_df.name = srcFolder
+      new_df.walk()
+      new_df.watch()
       @objects.push(new_df)

   checkFolder: (folderPath) ->


minor copy change

minor copy change

--- client/landing/app/Applications/Home.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Home.kdapplication/AppView.coffee
@@ -110,7 +110,7 @@

     @apiLink = new KDCustomHTMLView
       tagName   : "a"
-      partial   : "Api"
+      partial   : "API"
       tooltip   :
         title   : "Coming soon"


Update tokenized buffer specs

Update tokenized buffer specs

--- spec/tokenized-buffer-spec.coffee
+++ spec/tokenized-buffer-spec.coffee
@@ -1,7 +1,7 @@
 TokenizedBuffer = require '../src/tokenized-buffer'
 _ = require 'underscore-plus'

-describe "TokenizedBuffer", ->
+fdescribe "TokenizedBuffer", ->
   [tokenizedBuffer, buffer, changeHandler] = []

   beforeEach ->
@@ -335,12 +335,13 @@
         expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}"
         { tokens } = screenLine0

-        expect(tokens.length).toBe 3
+        expect(tokens.length).toBe 4
         expect(tokens[0].value).toBe "#"
         expect(tokens[1].value).toBe " Econ 101"
         expect(tokens[2].value).toBe tabAsSpaces
         expect(tokens[2].scopes).toEqual tokens[1].scopes
         expect(tokens[2].isAtomic).toBeTruthy()
+        expect(tokens[3].value).toBe ""

         expect(tokenizedBuffer.lineForScreenRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand"

@@ -446,11 +447,12 @@
       expect(screenLine1.text).toBe "//\uD835\uDF97xyz"
       { tokens } = screenLine1

-      expect(tokens.length).toBe 3
+      expect(tokens.length).toBe 4
       expect(tokens[0].value).toBe '//'
       expect(tokens[1].value).toBe '\uD835\uDF97'
       expect(tokens[1].value).toBeTruthy()
       expect(tokens[2].value).toBe 'xyz'
+      expect(tokens[3].value).toBe ''

   describe "when the grammar is tokenized", ->
     it "emits the `tokenized` event", ->


fix(contextualmenu): Disable for non-selectable inputs

  Fixes #664

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -164,6 +164,8 @@

   openContextualMenuForInput: (event) ->
     event.preventDefault()
+
+    return unless event.target.type in ['text', 'password', 'email', 'number', 'range', 'search', 'tel', 'url']
     hasSelectedText = event.target.selectionStart isnt event.target.selectionEnd

     remote = require('remote')


Update Collection.buildModel update existing model based on id_mapping_scope if exists instead of always building a new one

--- src/marbles/collection.js.coffee
+++ src/marbles/collection.js.coffee
@@ -38,7 +38,11 @@
         break

   @buildModel: (attrs) ->
-    new @model(attrs)
+    if @id_mapping_scope && @id_mapping_scope.length && _.every(@id_mapping_scope, ((k) => attrs.hasOwnProperty(k)))
+      model = @model.find(_.inject(@id_mapping_scope, ((m, k) => m[k] = attrs[k]; m), {}), fetch: false)
+      model.parseAttributes(attrs)
+    model ?= new @model(attrs)
+    model

   constructor: (@options = {}) ->
     @generateCid()


Use `Returns` keyword in `ThemeManager` public docs

Use the `Returns` keyword in `get*Theme` function docs so they are put
in panels like other return values in docs.

--- src/theme-manager.coffee
+++ src/theme-manager.coffee
@@ -71,11 +71,11 @@
   Section: Accessing Loaded Themes
   ###

-  # Public: Get an array of all the loaded theme names.
+  # Public: Returns an {Array} of {String}s of all the loaded theme names.
   getLoadedThemeNames: ->
     theme.name for theme in @getLoadedThemes()

-  # Public: Get an array of all the loaded themes.
+  # Public: Returns an {Array} of all the loaded themes.
   getLoadedThemes: ->
     pack for pack in @packageManager.getLoadedPackages() when pack.isTheme()

@@ -83,11 +83,11 @@
   Section: Accessing Active Themes
   ###

-  # Public: Get an array of all the active theme names.
+  # Public: Returns an {Array} of {String}s all the active theme names.
   getActiveThemeNames: ->
     theme.name for theme in @getActiveThemes()

-  # Public: Get an array of all the active themes.
+  # Public: Returns an {Array} of all the active themes.
   getActiveThemes: ->
     pack for pack in @packageManager.getActivePackages() when pack.isTheme()


fix(gruntfile.coffee): uglify -> compile_module_js now looks for JS files instead of COFFEE

--- gruntfile.coffee
+++ gruntfile.coffee
@@ -21,11 +21,15 @@
     (i.e. findModuleFilesIn('./src/app/') loads all module
     files in './src/app' in the defined order)
   ###
-  findModuleFilesIn = (modulePath) ->
+  findModuleFilesIn = (modulePath, isJS) ->
     output = []

     for globPath in gruntConfig.module_file_order
       output.push(modulePath + globPath)
+
+    if isJS
+      for script, i in output
+        output[i] = script.replace('coffee', 'js')

     return output

@@ -332,8 +336,8 @@
             except: ['exceptionLoggingService']
         src: [
           '<%= compile.vendor_js %>'
-          findModuleFilesIn('./build/src/app/')
-          findModuleFilesIn('./build/src/common/')
+          findModuleFilesIn('./build/src/app/', true)
+          findModuleFilesIn('./build/src/common/', true)
         ]
         dest: '<%= compile_dir %>/src/app/modules.min.js'
       compile_soft_uglify:


Fix bug with opt out of add to home screen

--- app/assets/javascripts/backbone/views/add_to_home_screen_view.coffee
+++ app/assets/javascripts/backbone/views/add_to_home_screen_view.coffee
@@ -33,7 +33,7 @@
   optOut: (event) ->
     event.preventDefault()
     @$el.hide()
-    document.cookie += "dontAddToHomeScreen=1;domain=.#{@options.host};" +
+    document.cookie = "dontAddToHomeScreen=1;domain=.#{@options.host};" +
       "path=/;expires=Tue, 19 Jan 2038 03:14:07 UTC;"

   optedOut: ->


[compute]: user data cloud init automation report error if no image selected

--- plugins/compute/app/assets/javascripts/compute/cloud_init_configuration.coffee
+++ plugins/compute/app/assets/javascripts/compute/cloud_init_configuration.coffee
@@ -30,6 +30,9 @@
   if os_image == "" || os_image == null
     attachPopover(button, 'Error', "Missing property 'vmware_ostype' on the image provided. Please follow the steps described in the documentation to upload a compatible image. See customer images documentation")
     return
+  if typeof os_image == "undefined"
+    attachPopover(button, 'Error', "Please choose an image.")
+    return
   osTypeWindows = os_image.search("windows")
   userDataFieldText = $('#server_user_data').val()

@@ -59,4 +62,4 @@
     element.popover('destroy')

 $ ->
-  $(document).on 'click','a[data-toggle="automationBootstrap"]', automationBootstrap+  $(document).on 'click','a[data-toggle="automationBootstrap"]', automationBootstrap


Show game board previews in game popover

--- static/script/pages/home.coffee
+++ static/script/pages/home.coffee
@@ -1,7 +1,7 @@
 window.settlers.templates.recent_events = {}
 window.settlers.templates.recent_events.playable = doT.template '
-
-

+
+

         {{? it.is_game}}


track clicks

track clicks

--- src/layout/mixins/actions.coffee
+++ src/layout/mixins/actions.coffee
@@ -42,6 +42,7 @@
       if sprite.get 'editable'
         send_message button.get('value')
         button.send 'decide'
+        button.send 'click'
       else
         button.send 'error', "#{sprite} not editable"
   )


Fixed a bug in payments js at backend

Fixed a bug in payments js at backend

fixes #6129

--- backend/app/assets/javascripts/spree/backend/payments/edit.js.coffee
+++ backend/app/assets/javascripts/spree/backend/payments/edit.js.coffee
@@ -3,7 +3,7 @@
   order_id = $('#payments').data('order-id')
   class Payment
     constructor: (id) ->
-      @url  = Spree.url("#{Spree.routes.payments_api(order_id)}/#{id}.json")
+      @url  = Spree.url("#{Spree.routes.payments_api(order_id)}/#{id}.json" + '?token=' + Spree.api_key)
       @json = $.getJSON @url.toString(), (data) =>
         @data = data


add hint

add hint

--- src/mailButler.appsscript.coffee
+++ src/mailButler.appsscript.coffee
@@ -18,6 +18,8 @@
       statusOK:                   'Service status: OK'
       scheduleSuccessCloseWindow: 'Your email has been scheduled, you can close this window now!'
       butSomethingWentWrong:      'But something went wrong: $status$'
+
+      # The following keys must not be changed - they are "error" + ErrorCodes.KEY
       errorMessageIdInvalid:      'Given message ID is not valid'
       errorNoScheduleTime:        'No scheduling time given'
       errorInvalidScheduleTime:   'Given scheduling time is not valid'


fixed the tab switch closes all tabs-bug (issue #190)

--- src/coffee/components/tabs.coffee
+++ src/coffee/components/tabs.coffee
@@ -4,10 +4,10 @@
     $this = $ this
     unless $this.hasClass('disabled')

-      if $this.parents('[role=tabpanel]').length > 0
-        tabs = $this.parents('[role=tabpanel]')
+      if $this.closest('[role=tabpanel]').length > 0
+        tabs = $this.closest('[role=tabpanel]')
       else
-        tabs = $this.parents('.tabs')
+        tabs = $this.closest('.tabs')

       tabs.find('> ul li a, [role=tab] a').removeClass('active')
       $this.addClass('active')


Fixes for collidable

Fixes for collidable

--- src/coffee/view/collidable.coffee
+++ src/coffee/view/collidable.coffee
@@ -20,7 +20,8 @@
 class View.Collidable extends View.Sprite
   constructor: (source, x, y, direction, additionalProperties) ->
     View.Sprite.call this, source, x, y, direction, additionalProperties
-    @mask = (if @mask then @mask else engine.loader.getMask(source, @getTheme()))
+    @mask = (if @mask then @mask else engine.
+    loader.getMask(source, @getTheme()))
     @collisionResolution = (if @collisionResolution then @collisionResolution else engine.defaultCollisionResolution)
     return


resurrection.activity: defer actual TokenView instantiation procedures

--- client/Social/Activity/ListItems/ActivityListItemStatusUpdate.coffee
+++ client/Social/Activity/ListItems/ActivityListItemStatusUpdate.coffee
@@ -34,6 +34,7 @@
     data = @getData()
     tagMap = @getTokenMap data.tags  if data.tags

+    viewParams = []
     for tokenString in tokenMatches
       [prefix, constructorName, id] = @decodeToken tokenString

@@ -47,8 +48,14 @@
       itemClass = tokenClassMap[prefix]
       tokenView = new TokenView {domId, itemClass}, token
       tokenView.emit "viewAppended"
+      str = str.replace tokenString, tokenView.getElement().outerHTML
+      tokenView.destroy()

-      str = str.replace tokenString, tokenView.getElement().outerHTML
+      viewParams.push {options: {domId, itemClass}, data: token}
+
+    @utils.defer ->
+      for {options, data} in viewParams
+        new TokenView options, data

     return  str


landing: check if environment isn't production

--- client/landing/site.landing/coffee/team/tabs/stripepaymenttab.coffee
+++ client/landing/site.landing/coffee/team/tabs/stripepaymenttab.coffee
@@ -23,7 +23,7 @@
       callback: @bound 'onSubmit'

     utils.loadStripe().then =>
-      if kd.config.environment is 'development'
+      if kd.config.environment isnt 'production'
         @submitWithDummyCard()


Add Collection::empty() and Collection::prependIds()

--- src/marbles/collection.js.coffee
+++ src/marbles/collection.js.coffee
@@ -16,6 +16,11 @@
     models = @appendRaw(resources_attribtues, silent: true)
     @trigger('reset', models)
     models
+
+  empty: =>
+    @model_ids = []
+    @trigger('reset', [])
+    []

   includes: (model) =>
     return @model_ids.indexOf(model.cid) != -1
@@ -51,6 +56,9 @@
     @trigger('prepend:complete', models)
     models

+  prependIds: (model_cids...) =>
+    @model_cids = model_cids.concat(@model_cids)
+
   first: =>
     return unless cid = @model_ids[0]
     @constructor.model.find({ cid: cid })


Handled 1st day of Shabuot and 7th day of Pesach on Friday explicitly

--- source/javascripts/mincha.coffee
+++ source/javascripts/mincha.coffee
@@ -176,7 +176,7 @@
     when @tonight_is_yom_tob() && @hebrew_date.isShabbat() then minutes_before_event(@sunset, 23)
     when @hebrew_date.is6thDayOfPesach() then @today().hour(18).minute(30)
     when @hebrew_date.isShabbat() then @mincha_on_shabbat()
-    when @yom_tob_that_we_can_pray_at_plag() && !@hebrew_date.isErebShabbat() then minutes_before_event(@plag(), 30)
+    when @yom_tob_that_we_can_pray_at_plag() then (if @hebrew_date.isErebShabbat() then @mincha_on_ereb_shabbat() else minutes_before_event(@plag(), 30))
     when @hebrew_date.isYomTob() && !@yom_tob_that_we_can_pray_at_plag() then minutes_before_event(@sunset, if @hebrew_date.isErebShabbat() then 40 else 25)
     when @hebrew_date.isErebYomTob() then minutes_before_event(@sunset, if @hebrew_date.isErebShabbat() then 33 else  25)
     when @hebrew_date.isErebShabbat() then @mincha_on_ereb_shabbat()


Update mocha server spec setup

Update mocha server spec setup

--- gulp/spec.coffee
+++ gulp/spec.coffee
@@ -4,6 +4,7 @@
 commonjs    = require 'gulp-wrap-commonjs'
 newer       = require 'gulp-newer'
 concat      = require 'gulp-concat'
+gutil       = require 'gulp-util'
 runSequence = require 'run-sequence'
 fs          = require 'fs'
 spawn       = require('child_process').spawn
@@ -32,8 +33,6 @@
       'coffee:coffee-script/register'
       '--reporter'
       'spec'
-      '--timeout',
-      '5000'
     ]
     if mochaProcess and mochaProcess.kill
       mochaProcess.kill()
@@ -41,12 +40,23 @@
       'node_modules/.bin/mocha'
       options.concat(glob)
       {}
-      next
     )
     mochaProcess.stdout.on 'data', (data) ->
       process.stdout.write data.toString()
     mochaProcess.stderr.on 'data', (data) ->
       process.stderr.write data.toString()
+    mochaProcess.on 'close', (code) ->
+      if code is 0
+        gutil.log gutil.colors.green "Finished: mocha server"
+        next()
+      else
+        errorMessage = "Failed: mocha server"
+        gutil.log gutil.colors.red errorMessage
+        gutil.beep()
+        if process.env.CI
+          process.exit 1
+        else
+          next()

   gulp.task 'spec:client', (next) ->


add Advantages wiki link to landing page

--- src/smc-webapp/landing_page.cjsx
+++ src/smc-webapp/landing_page.cjsx
@@ -466,8 +466,14 @@

"SageMathCloud provides a user-friendly interface. Students don’t need to install any software at all.
         They just open up a web browser and go to cloud.sagemath.com and that’s it. They just type code directly
         in, hit shift+enter and it runs, and they can see if it works. It provides immediate feedback. The course
-        management features work really well."

-
Quotes | Courses | Course management tutorial

+        management features work really well."
+

+

+            Quotes {' | '}
+            Courses {' | '}
+            Course management tutorial {' | '}
+            Advantages
+

 LandingPageContent = rclass


Fix typo

Fix typo

--- spec/deprecation-cop-spec.coffee
+++ spec/deprecation-cop-spec.coffee
@@ -8,7 +8,7 @@
     activationPromise = atom.packages.activatePackage('deprecation-cop')

   describe "when the deprecation-cop:view event is triggered", ->
-    it "displayes deprecation cop pane", ->
+    it "displays deprecation cop pane", ->
       expect(atom.workspace.getActivePane().getActiveItem()).not.toExist()

       atom.commands.dispatch workspaceElement, 'deprecation-cop:view'


fix(specs)

fix(specs)

--- spec/models/category-spec.coffee
+++ spec/models/category-spec.coffee
@@ -58,5 +58,5 @@
       cat.name = 'sent'
       expect(cat.isUserCategory()).toBe false
       expect(cat.isStandardCategory()).toBe true
-      expect(cat.isHiddenCategory()).toBe false
+      expect(cat.isHiddenCategory()).toBe true
       expect(cat.isLockedCategory()).toBe true


Missing lines

Missing lines

--- spec/storage-manager-spec.coffee
+++ spec/storage-manager-spec.coffee
@@ -380,8 +380,8 @@
           server.respond()
           expect(collection2.loaded).toBe true
           expect(collection2.length).toEqual 2
-#          expect(collection2.get(2).get('project').id).toEqual 10
-#          expect(collection2.get(3).get('project').id).toEqual 11
+          expect(collection2.get(2).get('project').id).toEqual 10
+          expect(collection2.get(3).get('project').id).toEqual 11

         it "does request ids from the server again when they don't have all associations loaded yet", ->
           respondWith server, "/api/time_entries?include=project&only=2",


Clean up accelerator note in js/moderation/scans

--- scanblog/static/js/moderation/scans.coffee
+++ scanblog/static/js/moderation/scans.coffee
@@ -123,6 +123,8 @@
         "license": ["#ff0"]
         "ignore": ["#000"]

+    # NOTE: These definitions are duplicated with event.keyCode below.  Here,
+    # they're used only to pick which letter to underline in the view.
     typeAccelerators:
         "post": "o"
         "profile": "f"
@@ -185,7 +187,6 @@
                     @choiceViews[index]._toggleChoice(event)

     loadSplit: (split) =>
-        console.log(split)
         @split = split
         @choices = []
         @typeCount = {}
@@ -196,7 +197,12 @@
                 @addChoice(new btb.Document type: type)

         # Add "ignored" choices if we have them.
-        @ignoreChoice = new btb.Document pages: [], choiceTitle: "ignore"
+        @ignoreChoice = new btb.Document({
+            pages: [],
+            choiceTitle: "ignore".replace(
+              new RegExp("(#{@typeAccelerators.ignore})"), "$1"
+            )
+        })
         if @split.get("scan").processing_complete
             @ignoreChoice.set(pages: @getUnassignedIds())


alias command

alias command

--- src/scripts/hubot-factoid.coffee
+++ src/scripts/hubot-factoid.coffee
@@ -43,9 +43,9 @@
       @name = name
       @tidbits = []
       @alias = false
-      @readonly = false
-      if data.readonly
-        @readonly = true
+      @readonly = true
+      if data.readonly? && data.readonly == false
+        @readonly = false
       if data.alias
         @alias = data.alias
       else if data.tidbits
@@ -130,6 +130,23 @@
         msg.send "I am " + output
       else
         msg.send [factoid.name, tidbit.verb, output].join ' '
+    alias: (msg) =>
+      src_name = msg.match[1].trim()
+      target_name = msg.match[2].trim()
+      src = robot.factoid.get src_name
+      if src
+        msg.reply "Sorry, there is already a factoid for '#{src_name}'."
+        return
+      target = robot.factoid.get target_name
+      if !target
+        msg.reply "Sorry, there is no factoid for the target '#{target_name}'."
+        return
+      msg.finish()
+      robot.logger.info "#{msg.message.user.name} aliased '#{src_name}' to '#{target_name}'"
+      factoid = @facts[src_name] = new Factoid(src_name)
+      factoid.alias = target_name
+      factoid.save()
+      msg.reply "Okay."
     setAddressed: (msg) =>
       msg.message.addressed = true
       @set msg
@@ -225,6 +242,8 @@
     factoid.readonly = protect
     factoid.save()
     msg.reply "Okay."
+
+  robot.respond /alias (.*?) => (.*?)$/, robot.factoid.alias

   robot.router.get "/#{robot.name}/factoid/:factoid", (req, res) ->
     factoid_name = req.params.factoid


GH-4: Pingdom Tools API changes

GH-4: Pingdom Tools API changes

--- src/scripts/pingdom-tools.coffee
+++ src/scripts/pingdom-tools.coffee
@@ -1,5 +1,5 @@
 # Description:
-#   Script for interacting with the Pingdom Tools.
+#   Script for interacting with Pingdom Tools.
 #
 # Dependencies:
 #   none
@@ -11,13 +11,18 @@
 #   hubot is <domain> (fast|slow)? - Request a Pingdom pagetest report on <domain>
 #   hubot (pagetest|fpt) <domain> - Request a Pingdom pagetest report on <domain>

+pkg = require('../../package.json')
+intervals = {}
+
 class PingdomTools

   full_page_test: (msg) ->
     my = this
     fpt_url = 'https://fpt-api.pingdom.com/api/0.1/test?save=false&url='
     fpt_site = msg.match[1]
-    msg.http("#{fpt_url}=#{fpt_site}")
+    msg.http("#{fpt_url}#{fpt_site}")
+      .header('referer', 'https://tools.pingdom.com/')
+      .header('user-agent', "hubot-pingdom-tools #{pkg.version} (#{pkg.homepage})")
       .get() (err, res, body) ->
         if err
           msg.send('Error: ' + err)
@@ -27,6 +32,8 @@
             if res.poll_state_url
               intervals[res.test_id] = setInterval () ->
                 msg.http(res.poll_state_url)
+                  .header('referer', 'https://tools.pingdom.com/')
+                  .header('user-agent', "hubot-pingdom-tools #{pkg.version} (#{pkg.homepage})")
                   .get() (err, res, poll_body) ->
                     if poll_body
                       poll_res = JSON.parse poll_body
@@ -49,6 +56,7 @@
                           msg.send(replies.join("\n"))
                         clearInterval intervals[res.test_id]
                     if err
+                      robot.log.error err, 'pingdom callback error'
                       msg.send('Error running Pingdom Full Page Test on ' + fpt_site)
                       clearInterval intervals[res.test_id]
               , 2000