Apply highlight trick to Project rows

Apply highlight trick to Project rows

--- src/smc-webapp/projects.cjsx
+++ src/smc-webapp/projects.cjsx
@@ -1062,6 +1062,14 @@
                            user_map    = {@props.user_map} />
         return r_join(users)

+    handle_mouse_down: (e) ->
+        @setState
+            highlighted_text : window.getSelection().toString()
+
+    handle_click: (e) ->
+        if window.getSelection().toString() == @state.highlighted_text
+            @open_project_from_list(e)
+
     open_project_from_list: (e) ->
         @actions('projects').open_project
             project_id : @props.project.project_id
@@ -1082,7 +1090,7 @@
             cursor          : 'pointer'
             wordWrap        : 'break-word'

-        <Well style={project_row_styles} onClick={@open_project_from_list}>
+        <Well style={project_row_styles} onClick={@handle_click} onMouseDown={@handle_mouse_down}>
             <Row>
                 <Col sm=3 style={fontWeight: 'bold', maxHeight: '7em', overflowY: 'auto'}>
                     <a>{html_to_text(@props.project.title)}</a>


Converted tabs to spaces

Converted tabs to spaces

--- src/spec/counter.spec.coffee
+++ src/spec/counter.spec.coffee
@@ -1,51 +1,51 @@

 describe 'Counter', ->
-	element = counter = null
-
-	beforeEach ->
-		element = document.createElement 'div'
+  element = counter = null
+
+  beforeEach ->
+    element = document.createElement 'div'

-		element.setAttribute 'data-duration', 10
-		element.setAttribute 'data-separator', ':'
+    element.setAttribute 'data-duration', 10
+    element.setAttribute 'data-separator', ':'

-		counter = new Counter element, 0, 5000, duration: 5, prefix: '£', suffix: ' Only', decimals: 2
+    counter = new Counter element, 0, 5000, duration: 5, prefix: '£', suffix: ' Only', decimals: 2

-	it 'should setup options using constructor, data attributes and default values', ->
-		expect(counter.options).toEqual
-			autostart: false
-			decimal: "."
-			decimals: 2
-			duration: 5
-			easing: true
-			grouping: true
-			prefix: "£"
-			separator: ":"
-			suffix: " Only"
+  it 'should setup options using constructor, data attributes and default values', ->
+    expect(counter.options).toEqual
+      autostart: false
+      decimal: "."
+      decimals: 2
+      duration: 5
+      easing: true
+      grouping: true
+      prefix: "£"
+      separator: ":"
+      suffix: " Only"

-	describe 'when the counter is started', ->
-		beforeEach ->
-			counter.start()
+  describe 'when the counter is started', ->
+    beforeEach ->
+      counter.start()

-		it 'should be running when started', ->
-			expect(counter.isRunning).toEqual yes
+    it 'should be running when started', ->
+      expect(counter.isRunning).toEqual yes

-	describe 'when the counter has been stopped', ->
-		beforeEach ->
-			counter.stop()
+  describe 'when the counter has been stopped', ->
+    beforeEach ->
+      counter.stop()

-		it 'should not be running when stopped', ->
-			expect(counter.isRunning).toBeFalsy()
+    it 'should not be running when stopped', ->
+      expect(counter.isRunning).toBeFalsy()

-		it 'should be possible to resume', ->
-			counter.resume()
-			expect(counter.isRunning).toBeTruthy()
+    it 'should be possible to resume', ->
+      counter.resume()
+      expect(counter.isRunning).toBeTruthy()

-	describe 'formatting numbers', ->
-		it 'should format postive numbers', ->
-			expect(counter.formatNumber 1000000).toEqual '£1:000:000.00 Only'
+  describe 'formatting numbers', ->
+    it 'should format postive numbers', ->
+      expect(counter.formatNumber 1000000).toEqual '£1:000:000.00 Only'

-		it 'should format negative numbers', ->
-			expect(counter.formatNumber -1000000).toEqual '£-1:000:000.00 Only'
+    it 'should format negative numbers', ->
+      expect(counter.formatNumber -1000000).toEqual '£-1:000:000.00 Only'

-		it 'should format numbers with decimals', ->
-			expect(counter.formatNumber 1000000.000).toEqual '£1:000:000.00 Only'
+    it 'should format numbers with decimals', ->
+      expect(counter.formatNumber 1000000.000).toEqual '£1:000:000.00 Only'


Fix unsubscriptions

Fix unsubscriptions

--- packages/rocketchat-mailer/server/functions/sendMail.coffee
+++ packages/rocketchat-mailer/server/functions/sendMail.coffee
@@ -14,7 +14,7 @@
 		# Meteor.users.find({ "username": /\.rocket\.team/ }).forEach (user) ->
 			email = user.emails?[0]?.address

-			html = body.replace /\[unsubscribe\]/g, Meteor.absoluteUrl(FlowRouter.path('rocket-mailer/unsubscribe/:hash', { hash: "#{user._id}:#{user.createdAt.getTime()}" }))
+			html = body.replace /\[unsubscribe\]/g, Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', { _id: user._id, createdAt: user.createdAt.getTime() }))
 			html = html.replace /\[name\]/g, user.name
 			fname = _.strLeft user.name, ' '
 			lname = _.strRightBack user.name, ' '
@@ -40,7 +40,7 @@
 		# Meteor.users.find({ "username": /\.rocket\.team/ }).forEach (user) ->
 			email = user.emails?[0]?.address

-			html = body.replace /\[unsubscribe\]/g, Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:hash', { hash: "#{user._id}:#{user.createdAt.getTime()}" }))
+			html = body.replace /\[unsubscribe\]/g, Meteor.absoluteUrl(FlowRouter.path('mailer/unsubscribe/:_id/:createdAt', { _id: user._id, createdAt: user.createdAt.getTime() }))
 			html = html.replace /\[name\]/g, user.name
 			fname = _.strLeft user.name, ' '
 			lname = _.strRightBack user.name, ' '


Apparently false != true

Apparently false != true

--- js-library/app/js/jail_iframe/util/annotated_site_receiver.coffee
+++ js-library/app/js/jail_iframe/util/annotated_site_receiver.coffee
@@ -21,7 +21,7 @@
     FactlinkJailRoot.$modalFrame.removeClass 'factlink-control-visible'
     setTimeout( ->
       return unless modalOpen
-      modalOpen = true
+      modalOpen = false
       FactlinkJailRoot.trigger 'modalClosed'
     , 300)


string_extensions

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -87,7 +87,6 @@

 "player_ammo".humanize()
 # => "Player ammo"
-
 </pre></code>

 @name humanize


Remove console.log

Remove console.log

--- src/course/invite-code-input.cjsx
+++ src/course/invite-code-input.cjsx
@@ -29,7 +29,6 @@
     </div>

   render: ->
-    console.log "Render: #{@props.course.isBusy}"
     button =
       <AsyncButton
         isWaiting={@props.course.isBusy}


Added authorizePublish() method.

Added authorizePublish() method.

--- src/ProjectUtils.coffee
+++ src/ProjectUtils.coffee
@@ -145,3 +145,13 @@
       throw new Meteor.Error(403, 'No user provided')
     unless AccountsUtil.isOwner(Projects.findOne(projectId), userId)
       throw new Meteor.Error(403, 'User not authorized to view project collections.')
+
+  authorizePublish: (projectId, callback) ->
+    # Ignore the request if no user ID exists.
+    unless @userId then return []
+    try
+      ProjectUtils.assertAuthorization(projectId, @userId)
+      callback.call(@)
+    catch e
+      Logger.error('Error in publications', e, e.stack)
+      @error(e)


model_postgres: fix getNodeListeners() SQL

--- src/local/model_postgres.coffee
+++ src/local/model_postgres.coffee
@@ -293,7 +293,7 @@
         ], cb

     getNodeListeners: (node, cb) ->
-        @db.query "SELECT DISTINCT listener FROM subscriptions WHERE node = $1 AND listener NOT NULL"
+        @db.query "SELECT DISTINCT listener FROM subscriptions WHERE node = $1 AND listener IS NOT NULL"
         , [node]
         , (err, res) ->
             cb err, res?.rows?.map((row) -> row.listener)


robots should be seen and not heard

robots should be seen and not heard

--- src/scripts/maps.coffee
+++ src/scripts/maps.coffee
@@ -4,7 +4,7 @@

 module.exports = (robot) ->

-  robot.hear /(?:(satellite|terrain|hybrid)[- ])?map me (.+)/i, (msg) ->
+  robot.respond /(?:(satellite|terrain|hybrid)[- ])?map me (.+)/i, (msg) ->
     mapType  = msg.match[1] || "roadmap"
     location = msg.match[2]
     mapUrl      = "http://maps.google.com/maps/api/staticmap?markers=" +


Google analytics

Google analytics

--- src/scripts/site.coffee
+++ src/scripts/site.coffee
@@ -20,7 +20,7 @@
     talkHost: 'http://talk.batdetective.org'

     domain: 'batdetective.org'
-    # googleAnalytics: 'X'
+    googleAnalytics: 'UA-1224199-32'

     cartoUser: 'CARTODB USER'
     cartoApiKey: 'CARTODB API KEY'


Need a favicon eventually

Need a favicon eventually

--- src/server/index.coffee
+++ src/server/index.coffee
@@ -28,7 +28,8 @@
 publicPath = path.join root, 'public'

 expressApp
-  .use(express.favicon('#{publicPath}/favicon.ico'))
+  # TODO: favicon
+  #.use(express.favicon('#{publicPath}/favicon.ico'))

   # Gzip static files and serve from memory
   .use(gzippo.staticGzip publicPath, maxAge: ONE_YEAR)


update spec

update spec

--- test/components/helpers/calendar/checks.coffee
+++ test/components/helpers/calendar/checks.coffee
@@ -128,7 +128,7 @@
   _checkAddPlansWarning: ({div, component, state, router, history, courseId}) ->
     addOnDayDropdown = React.addons.TestUtils.findRenderedComponentWithType(component, Add)
     expect(addOnDayDropdown.getDOMNode().style.display).to.not.equal('none')
-    expect(addOnDayDropdown.getDOMNode().innerText).to.contain('Cannot add')
+    expect(addOnDayDropdown.getDOMNode().innerText).to.contain('Cannot assign')

     {div, component, state, router, history, courseId}


when linking an account, at least redirect new window to cocalc... (maybe good; not sure)

--- src/smc-hub/auth.coffee
+++ src/smc-hub/auth.coffee
@@ -129,6 +129,7 @@
         has_valid_remember_me : false
         account_id            : undefined
         email_address         : undefined
+        target                : BASE_URL + "/app#login"
         cookies               : new Cookies(opts.req, opts.res)

     ## dbg("cookies = '#{opts.req.headers['cookie']}'")  # DANGER -- do not uncomment except for debugging due to SECURITY
@@ -344,7 +345,6 @@
                 return

             dbg("passport created: set remember_me cookie, so user gets logged in")
-            locals.target = BASE_URL + "/app#login"

             # create and set remember_me cookie, then redirect.
             # See the remember_me method of client for the algorithm we use.
@@ -375,11 +375,9 @@
     ], (err) ->
         if err
             opts.res.send("Error trying to login using #{opts.strategy} -- #{err}")
-        else if locals.target?
+        else
             dbg("redirect the client")
             opts.res.redirect(locals.target)
-        else
-            opts.res.send("signed in")
         opts.cb?(err)
     )


Extend decorations to start of next line

This ensures that the git-line-added decoration is rendered on the last
line of a multi-line section, even if that line is empty. Previously
this didn’t work because non-emtpy decorations don’t show on the last
line if its column is 0.

--- lib/git-diff-view.coffee
+++ lib/git-diff-view.coffee
@@ -107,7 +107,7 @@
   addDecorations: (diffs) ->
     for {oldStart, newStart, oldLines, newLines} in diffs
       startRow = newStart - 1
-      endRow = newStart + newLines - 2
+      endRow = newStart + newLines - 1
       if oldLines is 0 and newLines > 0
         @markRange(startRow, endRow, 'git-line-added')
       else if newLines is 0 and oldLines > 0
@@ -121,6 +121,6 @@
     @markers = []

   markRange: (startRow, endRow, klass) ->
-    marker = @editor.markBufferRange([[startRow, 0], [endRow, Infinity]], invalidate: 'never')
+    marker = @editor.markBufferRange([[startRow, 0], [endRow, 0]], invalidate: 'never')
     @editor.decorateMarker(marker, type: 'line-number', class: klass)
     @markers.push(marker)


remove parentheses

remove parentheses

--- src/angular-lightbox.coffee
+++ src/angular-lightbox.coffee
@@ -2,7 +2,7 @@

 this.angular.module "angularLightbox", []
 	.directive "lbCellRoot", [
-		() ->
+		->
 			link = (scope, element, attrs) ->
 				element.css
 					listStyle: "none"
@@ -15,7 +15,7 @@
 			link: link
 	]
 	.directive "lbCellRoom", [
-		() ->
+		->
 			link = (scope, element, attrs) ->
 				element.css
 					display: "inline-block"
@@ -49,7 +49,7 @@
 			link: link
 	]
 	.directive "lbCellImg", [
-		() ->
+		->
 			link = (scope, element, attrs) ->
 				element.css
 					display: "block"
@@ -58,7 +58,7 @@
 			link: link
 	]
 	.directive "lbOverlay", [
-		() ->
+		->
 			link = (scope, element, attrs) ->
 				element.css
 					position: "absolute"
@@ -76,7 +76,7 @@
 			link: link
 	]
 	.directive "lbOverlayImg", [
-		() ->
+		->
 			link = (scope, element, attrs) ->
 				element.css
 					marginTop: "10%"
@@ -84,7 +84,7 @@
 			link: link
 	]
 	.directive "lbCell", [
-		() ->
+		->
 			restrict = "AE"
 			template = '
 				<li lb-cell-room ng-repeat="lb in lightboxImgs">


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/array_extensions.coffee
+++ src/array_extensions.coffee
@@ -1,5 +1,5 @@
 ###*
-Calculate the average value
+Calculate the average value of an arr

 [null, undefined, 3, 3, undefined, 5].compact()


fix(db): Don't blow up over table SCANs with covering indexes

--- src/flux/stores/database-store.coffee
+++ src/flux/stores/database-store.coffee
@@ -273,10 +273,10 @@
         if DEBUG_QUERY_PLANS
           @_db.all "EXPLAIN QUERY PLAN #{query}", values, (err, results=[]) =>
             str = results.map((row) -> row.detail).join('\n') + " for " + query
-            return if str.indexOf("SCAN") is -1
             return if str.indexOf('ThreadCounts') > 0
             return if str.indexOf('ThreadSearch') > 0
-            @_prettyConsoleLog(str)
+            if str.indexOf('SCAN') isnt -1 and str.indexOf('COVERING INDEX') is -1
+              @_prettyConsoleLog(str)

       # Important: once the user begins a transaction, queries need to run
       # in serial.  This ensures that the subsequent "COMMIT" call


Adding copyright sig to deparam

Adding copyright sig to deparam

--- app/assets/javascripts/lib/utils/deparam.coffee
+++ app/assets/javascripts/lib/utils/deparam.coffee
@@ -1,6 +1,8 @@
 # Taken from Ben Alman's BBQ library and converted to CS
 # Explaine: http://benalman.com/code/projects/jquery-bbq/examples/deparam/
 # Source: https://github.com/cowboy/jquery-bbq/blob/master/jquery.ba-bbq.js#L466
+# Copyright (c) 2010 "Cowboy" Ben Alman
+# LICENSE: https://raw.githubusercontent.com/cowboy/jquery-bbq/a1cfc5347057762cdb9fbeb19c0acdd629f569c6/LICENSE-MIT

 define ["jquery"], (jQuery) ->
   decode = decodeURIComponent


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -1,3 +1,4 @@
 TimedEvents = (I={}) ->
   every: (n) ->
-
+    if I.age.mod(5) is 0
+      doSomething()


sending all events to particles

sending all events to particles

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -9,7 +9,7 @@
 ###
 TimedEvents = (I={}) ->
   Object.reverseMerge I,
-
+    every
   ###*
   Execute fn every n frames.


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -44,7 +44,7 @@
       period: period
       lastFired: I.age

-  # TODO: Move this into a more
+  # TODO: Move this into a more core module
   sendOrApply: (fn) ->
     if typeof fn is "function"
       fn.apply(self)


Revert changes in js used to create project select

This will preserve those changes in history, we may need them in future.
Currently we will use a issues helpers to create options to choose
project whem moving issue to it.

--- app/assets/javascripts/project_select.js.coffee
+++ app/assets/javascripts/project_select.js.coffee
@@ -1,12 +1,9 @@
 class @ProjectSelect
   constructor: ->
-    $('.ajax-project-select').each (i, select) =>
+    $('.ajax-project-select').each (i, select) ->
       @groupId = $(select).data('group-id')
       @includeGroups = $(select).data('include-groups')
       @orderBy = $(select).data('order-by') || 'id'
-      @selectId = $(select).data('select-id') || 'web_url'
-      @accessLevel = $(select).data('access-level')
-      @withoutId = $(select).data('without-id')

       placeholder = "Search for project"
       placeholder += " or group" if @includeGroups
@@ -14,57 +11,30 @@
       $(select).select2
         placeholder: placeholder
         minimumInputLength: 0
-        query: (options) =>
+        query: (query) =>
+          finalCallback = (projects) ->
+            data = { results: projects }
+            query.callback(data)
+
+          if @includeGroups
+            projectsCallback = (projects) ->
+              groupsCallback = (groups) ->
+                data = groups.concat(projects)
+                finalCallback(data)
+
+              Api.groups query.term, false, groupsCallback
+          else
+            projectsCallback = finalCallback
+
           if @groupId
-            Api.groupProjects @groupId, options.term, @createCallback(options)
+            Api.groupProjects @groupId, query.term, projectsCallback
           else
-            Api.projects options.term, @orderBy, @createCallback(options)
+            Api.projects query.term, @orderBy, projectsCallback

-        id: (project) =>
-          project[@selectId]
+        id: (project) ->
+          project.web_url

         text: (project) ->
           project.name_with_namespace || project.name

         dropdownCssClass: "ajax-project-dropdown"
-
-  createCallback: (options) =>
-    finalCallback = (projects) ->
-      options.callback({ results: projects })
-
-    @withoutIdCallbackDecorator(
-      @accessLevelCallbackDecorator(
-        @groupsCallbackDecorator(
-          finalCallback
-        )
-      )
-    )
-
-  groupsCallbackDecorator: (callback) =>
-    return callback unless @includeGroups
-
-    (projects) =>
-      Api.groups options.term, false, (groups) =>
-        data = groups.concat(projects)
-        callback(data)
-
-  accessLevelCallbackDecorator: (callback) =>
-    return callback unless @accessLevel
-
-    (projects) =>
-      data = projects.filter (p) =>
-        max = Math.max(p.permissions.group_access?.access_level ? 0,
-                       p.permissions.project_access?.access_level ? 0)
-
-        max >= @accessLevel
-
-      callback(data)
-
-  withoutIdCallbackDecorator: (callback) =>
-    return callback unless @withoutId
-
-    (projects) =>
-      data = projects.filter (p) =>
-        p.id != @withoutId
-
-      callback(data)


FF fix

FF fix

--- app/assets/javascripts/projects/tasks.js.coffee
+++ app/assets/javascripts/projects/tasks.js.coffee
@@ -47,7 +47,7 @@
     task = @getTask(item.id)
     task.name = item.name
     task.description = item.description
-    task.tags = item.tags
+    task.tag_list = item.tag_list
     task.assignee_id = item.assignee_id
     task.original_estimate = item.original_estimate
     task.remaining_estimate = item.remaining_estimate
@@ -233,8 +233,8 @@
       afterChange: (changes, source) ->
         Tasks.updateSummary()

-        if source == 'loadData'
-          return
+        return if source != 'edit'
+
         for change in changes
           if change[1] != 'delta'
             instance = grid.handsontable('getInstance')
@@ -243,8 +243,9 @@
             else
               physicalIndex = change[0]
             item = instance.getDataAtRow(physicalIndex)
-            Api.update_task gon.project_id, item.id, item, (task) ->
-              # nothing to do here
+            if item?
+              Api.update_task gon.project_id, item.id, item, (task) ->
+                # nothing to do here

       cells: (row, col, prop) =>
         cellProperties = {}


messing with travisci

messing with travisci

--- test/cases/controller/paramsTest.coffee
+++ test/cases/controller/paramsTest.coffee
@@ -2,18 +2,10 @@

 describe "Tower.ControllerParams", ->
   beforeEach (done) ->
-    # for travisci...
-    console.log 'Tower.ControllerParams.beforeEach...'
+    # for travisci... for some reason there is a post created, not sure why
     App.Post.destroy =>
-      App.Post.all (error, posts) =>
-        console.log 'BEFORE ANY POSTS', _.stringify(posts) #
-        App.Post.create title: 'First Post', rating: 8, =>
-          App.Post.all (error, posts) =>
-            console.log 'After FIRST POST', _.stringify(posts) #
-            App.Post.create title: 'Second Post', rating: 7, =>
-              App.Post.all (error, posts) =>
-                console.log 'After SECOND POST', _.stringify(posts) #
-                done()
+      App.Post.create title: 'First Post', rating: 8, =>
+        App.Post.create title: 'Second Post', rating: 7, done

   beforeEach (done) ->
     Tower.start(done)


fix edit in a modal

fix edit in a modal

--- src/js/controllers/journal.coffee
+++ src/js/controllers/journal.coffee
@@ -32,6 +32,7 @@
         $location.url("/memories/#{item.id}")
       if dialog
         dialog.$scope.$on 'editItem', (event) ->
+          dialog.$scope.$hide()
           dialog.$scope.$destroy()
           $timeout ->
             $scope.editItemUsingModal(type, item)


improved bubbles label logic

improved bubbles label logic

--- src/viz/types/bubbles.coffee
+++ src/viz/types/bubbles.coffee
@@ -9,12 +9,6 @@

   groupedData = groupData vars, vars.data.viz

-  groupedData = [groupedData[40], groupedData[41]]
-
-  # Test for labels
-  maxChildren = d3.max groupedData, (d) -> if d.values instanceof Array then d.values.length else 1
-  labelHeight = if vars.labels.value and not vars.small and maxChildren >= 1 then 50 else 0
-  console.log maxChildren, labelHeight
   # Calculate rows and columns
   dataLength = groupedData.length
   if dataLength < 4
@@ -42,6 +36,7 @@

   padding  = 5
   size_max = (d3.min([column_width, column_height]) / 2) - (padding * 2)
+  labelHeight = if vars.labels.value and not vars.small and size_max >= 40 then d3.max([20, d3.min [size_max * 0.25, 50]]) else 0
   size_max -= labelHeight
   size_min = d3.min [size_max, vars.size.scale.min.value]

@@ -96,12 +91,13 @@

     d.d3plus.static = d.d3plus.depth < vars.depth.value and vars.id.grouping.value

-    if d.d3plus.depth is 0 or vars.id.grouping.value is false
+    if labelHeight and (d.d3plus.depth is 0 or vars.id.grouping.value is false)
       d.d3plus.label =
         x: 0
         y: -(size_max + labelHeight / 2)
-        w: size_max * 1.5
+        w: size_max * 2
         h: labelHeight
+        padding: 0
         color: legible(fetchColor(vars, d, d.d3plus.depth))
     else
       delete d.d3plus.label


Click on buttons to remove

Click on buttons to remove

--- src/layout/mixins/programs.coffee
+++ src/layout/mixins/programs.coffee
@@ -69,18 +69,13 @@
       console.log "programs store: #{world}, {#{action}}"
       actions_container = world.find_child('actions')
       actions_container.add_child action
-
-    clear: (world, action) ->
-      console.log "programs clear: #{world}, {#{action}}"
-      actions_container = world.find_child('actions')
-      index = action.index
-      #actions_container.remove_child(index)
   }

   program_row = (name, contents) ->
     program = make.columns name, [
       { _LABEL: 'program_name', name: name }
-      make.buttons("action", contents, my.command)
+      make.buttons "action", contents, my.command, (button, args) -> button.up.remove_child(button)
+
     ]
     my.extend program, program_behavior()


Minor test fix.

Minor test fix.

--- test/specs/controllers/Dashboard.coffee
+++ test/specs/controllers/Dashboard.coffee
@@ -50,7 +50,7 @@
       for i in [1..5]
         @dashboard.createTool Table

-      @tool = @dashboard.tools[2].channel
+      @tool = @dashboard.tools[2]
       @dashboard.removeTool @tool

     it 'should remove the specified tool from the list of published channels', ->


Minor: typo and avoid code duplication

Minor: typo and avoid code duplication

--- src/resources-packaged/ops/javascript/orbeon/util/String.coffee
+++ src/resources-packaged/ops/javascript/orbeon/util/String.coffee
@@ -10,7 +10,7 @@
 #
 # The full text of the license is available at http://www.gnu.org/copyleft/lesser.html

-# TODO: should most likel be called StringUtils rather than String, to avoid overloading String when imported
+# TODO: should most likely be called StringUtils rather than String, to avoid overloading String when imported
 ORBEON.util.String = _.tap {}, (StringUtils) -> _.extend StringUtils,

     # Based on code by Fagner Brack, under MIT license
@@ -30,9 +30,8 @@
     # Evaluates JavaScript which can contain return characters we need to remove
     # TODO: Why do we need to replace newline in JavaScript before it to eval()?
     eval: (jsString) ->
-        jsString = StringUtils.replace jsString, "\n", " "
-        jsString = StringUtils.replace jsString, "\r", " "
-        eval jsString
+        replace = (text, token) -> StringUtils.replace text, token, " "
+        eval (_.foldl ['\n', '\r'], replace, jsString)

     # Escape text that appears in an HTML attribute which we use in an innerHTML
     escapeForMarkup: (text) ->


consume health status from docker inspect

--- imports/server/mqtt/docker/inspect.coffee
+++ imports/server/mqtt/docker/inspect.coffee
@@ -5,6 +5,12 @@
   if msg.State?.ExitCode is 0
     'stopped'
   else 'failed'
+
+mapHealthStatus = (status) ->
+  switch status
+    when 'healthy' then status
+    when 'unhealthy' then status
+    else 'unknown'

 module.exports = (msg) ->
   if labels = msg.Config?.Labels
@@ -29,219 +35,220 @@
       reconciler.updateContainerId id, labels
     if created = msg.Created
       reconciler.updateCreated created, labels
+    if Health = msg.State?.Health
+      reconciler.updateHealthStatus (mapHealthStatus Health.Status), labels

 ###
-{
-   "Id":"2ed8eb5dc8accf6f9f02816254807b03955562429023402004a84123c12f5b41",
-   "Created":"2016-09-01T13:49:04.896382759Z",
-   "Path":"docker-entrypoint.sh",
-   "Args":[
-      "npm",
-      "start"
-   ],
-   "State":{
-      "Status":"running",
-      "Running":true,
-      "Paused":false,
-      "Restarting":false,
-      "OOMKilled":false,
-      "Dead":false,
-      "Pid":10188,
-      "ExitCode":0,
-      "Error":"",
-      "StartedAt":"2016-09-01T13:49:06.541456436Z",
-      "FinishedAt":"0001-01-01T00:00:00Z"
-   },
-   "Image":"sha256:2f262d04e17f7f081d05500d9453deaf9d866a5fa9a1c54cd3f0e6f0c63adab3",
-   "ResolvConfPath":"/var/lib/docker/containers/2ed8eb5dc8accf6f9f02816254807b03955562429023402004a84123c12f5b41/resolv.conf",
-   "HostnamePath":"/var/lib/docker/containers/2ed8eb5dc8accf6f9f02816254807b03955562429023402004a84123c12f5b41/hostname",
-   "HostsPath":"/var/lib/docker/containers/2ed8eb5dc8accf6f9f02816254807b03955562429023402004a84123c12f5b41/hosts",
-   "LogPath":"/var/lib/docker/containers/2ed8eb5dc8accf6f9f02816254807b03955562429023402004a84123c12f5b41/2ed8eb5dc8accf6f9f02816254807b03955562429023402004a84123c12f5b41-json.log",
-   "Name":"/dashboard-agent-infra",
-   "RestartCount":0,
-   "Driver":"btrfs",
-   "MountLabel":"",
-   "ProcessLabel":"",
-   "AppArmorProfile":"",
-   "ExecIDs":null,
-   "HostConfig":{
-      "Binds":[
-         "/var/run/docker.sock:/var/run/docker.sock",
-         "/local/data:/local/data",
-         "/mnt/data:/mnt/data"
-      ],
-      "ContainerIDFile":"",
-      "LogConfig":{
-         "Type":"json-file",
-         "Config":{
-
-         }
-      },
-      "NetworkMode":"none",
-      "PortBindings":{
-
-      },
-      "RestartPolicy":{
-         "Name":"no",
-         "MaximumRetryCount":0
-      },
-      "VolumeDriver":"",
-      "VolumesFrom":null,
-      "CapAdd":null,
-      "CapDrop":null,
-      "Dns":[
-
-      ],
-      "DnsOptions":[
-
-      ],
-      "DnsSearch":[
-
-      ],
-      "ExtraHosts":null,
-      "GroupAdd":null,
-      "IpcMode":"",
-      "Links":null,
-      "OomScoreAdj":0,
-      "PidMode":"",
-      "Privileged":false,
-      "PublishAllPorts":false,
-      "ReadonlyRootfs":false,
-      "SecurityOpt":null,
-      "UTSMode":"",
-      "ShmSize":67108864,
-      "ConsoleSize":[
-         0,
-         0
-      ],
-      "Isolation":"",
-      "CpuShares":0,
-      "CgroupParent":"",
-      "BlkioWeight":0,
-      "BlkioWeightDevice":null,
-      "BlkioDeviceReadBps":null,
-      "BlkioDeviceWriteBps":null,
-      "BlkioDeviceReadIOps":null,
-      "BlkioDeviceWriteIOps":null,
-      "CpuPeriod":0,
-      "CpuQuota":0,
-      "CpusetCpus":"",
-      "CpusetMems":"",
-      "Devices":[
-
-      ],
-      "KernelMemory":0,
-      "Memory":0,
-      "MemoryReservation":0,
-      "MemorySwap":0,
-      "MemorySwappiness":-1,
-      "OomKillDisable":false,
-      "PidsLimit":0,
-      "Ulimits":null
-   },
-   "GraphDriver":{
-      "Name":"btrfs",
-      "Data":null
-   },
-   "Mounts":[
-      {
-         "Source":"/mnt/data",
-         "Destination":"/mnt/data",
-         "Mode":"",
-         "RW":true,
-         "Propagation":"rprivate"
-      },
-      {
-         "Source":"/var/run/docker.sock",
-         "Destination":"/var/run/docker.sock",
-         "Mode":"",
-         "RW":true,
-         "Propagation":"rprivate"
-      },
-      {
-         "Source":"/local/data",
-         "Destination":"/local/data",
-         "Mode":"",
-         "RW":true,
-         "Propagation":"rprivate"
-      }
-   ],
-   "Config":{
-      "Hostname":"248",
-      "Domainname":"agent.dashboard.infra.ictu",
-      "User":"",
-      "AttachStdin":false,
-      "AttachStdout":true,
-      "AttachStderr":true,
-      "ExposedPorts":{
-         "80/tcp":{
-
-         }
-      },
-      "Tty":false,
-      "OpenStdin":false,
-      "StdinOnce":false,
-      "Env":[
-         "eth0_pipework_cmd=ens192 -i eth0 @CONTAINER_NAME@ dhclient @3088",
-         "AUTH_TOKEN=Ena9Mu",
-         "BASE_DIR=/local/data/scripts",
-         "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
-         "DOCKER_BUCKET=get.docker.com",
-         "DOCKER_VERSION=1.9.1",
-         "DOCKER_SHA256=52286a92999f003e1129422e78be3e1049f963be1888afc3c9a99d5a9af04666"
-      ],
-      "Cmd":[
-         "npm",
-         "start"
-      ],
-      "Image":"ictu/dashboard-agent:4",
-      "Volumes":null,
-      "WorkingDir":"/app",
-      "Entrypoint":[
-         "docker-entrypoint.sh"
-      ],
-      "OnBuild":null,
-      "Labels":{
-
-      },
-      "StopSignal":"SIGTERM"
-   },
-   "NetworkSettings":{
-      "Bridge":"",
-      "SandboxID":"481efa66591e7467edbad71c64fb114bdd45faceaaaead91450c4d9cd0ba3b9c",
-      "HairpinMode":false,
-      "LinkLocalIPv6Address":"",
-      "LinkLocalIPv6PrefixLen":0,
-      "Ports":{
-
-      },
-      "SandboxKey":"/var/run/docker/netns/481efa66591e",
-      "SecondaryIPAddresses":null,
-      "SecondaryIPv6Addresses":null,
-      "EndpointID":"",
-      "Gateway":"",
-      "GlobalIPv6Address":"",
-      "GlobalIPv6PrefixLen":0,
-      "IPAddress":"",
-      "IPPrefixLen":0,
-      "IPv6Gateway":"",
-      "MacAddress":"",
-      "Networks":{
-         "none":{
-            "IPAMConfig":null,
-            "Links":null,
-            "Aliases":null,
-            "NetworkID":"b84f9298faf8500e923ce58c418ed8ddb39d017faf7216fce21956ff84043eb2",
-            "EndpointID":"ba6a5ffc05b349db1c61341f1cb20609235db70ca97eb2c88acd932f2a57a6a2",
-            "Gateway":"",
-            "IPAddress":"",
-            "IPPrefixLen":0,
-            "IPv6Gateway":"",
-            "GlobalIPv6Address":"",
-            "GlobalIPv6PrefixLen":0,
-            "MacAddress":""
-         }
-      }
-   }
-}
+[
+    {
+        "Id": "30b496239b6884730f0a92b97b6d82454b6d981d8e0f0032e50a5b8e9d0d8e5c",
+        "Created": "2017-03-09T10:22:34.264338437Z",
+        "Path": "nginx",
+        "Args": [
+            "-g",
+            "daemon off;"
+        ],
+        "State": {
+            "Status": "running",
+            "Running": true,
+            "Paused": false,
+            "Restarting": false,
+            "OOMKilled": false,
+            "Dead": false,
+            "Pid": 23173,
+            "ExitCode": 0,
+            "Error": "",
+            "StartedAt": "2017-03-09T10:22:35.055390517Z",
+            "FinishedAt": "0001-01-01T00:00:00Z",
+            "Health": {
+                "Status": "healthy",
+                "FailingStreak": 0,
+                "Log": [
+                    {
+                        "Start": "2017-03-09T11:23:05.137610232+01:00",
+                        "End": "2017-03-09T11:23:05.20778855+01:00",
+                        "ExitCode": 0,
+                        "Output": "hallo\n"
+                    },
+                    {
+                        "Start": "2017-03-09T11:23:35.208126372+01:00",
+                        "End": "2017-03-09T11:23:35.324496295+01:00",
+                        "ExitCode": 0,
+                        "Output": "hallo\n"
+                    },
+                    {
+                        "Start": "2017-03-09T11:24:05.324667895+01:00",
+                        "End": "2017-03-09T11:24:05.376264535+01:00",
+                        "ExitCode": 0,
+                        "Output": "hallo\n"
+                    },
+                    {
+                        "Start": "2017-03-09T11:24:35.376476629+01:00",
+                        "End": "2017-03-09T11:24:35.430625088+01:00",
+                        "ExitCode": 0,
+                        "Output": "hallo\n"
+                    }
+                ]
+            }
+        },
+        "Image": "sha256:6b914bbcb89e49851990e064568dceee4d53a462f316ec36207599c12ae9ba65",
+        "ResolvConfPath": "/var/lib/docker/containers/6f37971c1b296c4699c26b128aefac47ade75d654d0d36232ed96e0a1fdd7c08/resolv.conf",
+        "HostnamePath": "/var/lib/docker/containers/6f37971c1b296c4699c26b128aefac47ade75d654d0d36232ed96e0a1fdd7c08/hostname",
+        "HostsPath": "/var/lib/docker/containers/6f37971c1b296c4699c26b128aefac47ade75d654d0d36232ed96e0a1fdd7c08/hosts",
+        "LogPath": "/var/lib/docker/containers/30b496239b6884730f0a92b97b6d82454b6d981d8e0f0032e50a5b8e9d0d8e5c/30b496239b6884730f0a92b97b6d82454b6d981d8e0f0032e50a5b8e9d0d8e5c-json.log",
+        "Name": "/infra-nginx-healtcheck-www",
+        "RestartCount": 0,
+        "Driver": "btrfs",
+        "MountLabel": "",
+        "ProcessLabel": "",
+        "AppArmorProfile": "",
+        "ExecIDs": null,
+        "HostConfig": {
+            "Binds": [],
+            "ContainerIDFile": "",
+            "LogConfig": {
+                "Type": "json-file",
+                "Config": {}
+            },
+            "NetworkMode": "container:6f37971c1b296c4699c26b128aefac47ade75d654d0d36232ed96e0a1fdd7c08",
+            "PortBindings": {},
+            "RestartPolicy": {
+                "Name": "unless-stopped",
+                "MaximumRetryCount": 0
+            },
+            "AutoRemove": false,
+            "VolumeDriver": "",
+            "VolumesFrom": [],
+            "CapAdd": null,
+            "CapDrop": null,
+            "Dns": null,
+            "DnsOptions": null,
+            "DnsSearch": null,
+            "ExtraHosts": null,
+            "GroupAdd": null,
+            "IpcMode": "",
+            "Cgroup": "",
+            "Links": null,
+            "OomScoreAdj": 0,
+            "PidMode": "",
+            "Privileged": false,
+            "PublishAllPorts": false,
+            "ReadonlyRootfs": false,
+            "SecurityOpt": null,
+            "UTSMode": "",
+            "UsernsMode": "",
+            "ShmSize": 67108864,
+            "Runtime": "runc",
+            "ConsoleSize": [
+                0,
+                0
+            ],
+            "Isolation": "",
+            "CpuShares": 0,
+            "Memory": 0,
+            "CgroupParent": "",
+            "BlkioWeight": 0,
+            "BlkioWeightDevice": null,
+            "BlkioDeviceReadBps": null,
+            "BlkioDeviceWriteBps": null,
+            "BlkioDeviceReadIOps": null,
+            "BlkioDeviceWriteIOps": null,
+            "CpuPeriod": 0,
+            "CpuQuota": 0,
+            "CpusetCpus": "",
+            "CpusetMems": "",
+            "Devices": null,
+            "DiskQuota": 0,
+            "KernelMemory": 0,
+            "MemoryReservation": 0,
+            "MemorySwap": 0,
+            "MemorySwappiness": -1,
+            "OomKillDisable": false,
+            "PidsLimit": 0,
+            "Ulimits": null,
+            "CpuCount": 0,
+            "CpuPercent": 0,
+            "IOMaximumIOps": 0,
+            "IOMaximumBandwidth": 0
+        },
+        "GraphDriver": {
+            "Name": "btrfs",
+            "Data": null
+        },
+        "Mounts": [],
+        "Config": {
+            "Hostname": "www",
+            "Domainname": "nginx-healtcheck.innovation.ictu",
+            "User": "",
+            "AttachStdin": false,
+            "AttachStdout": false,
+            "AttachStderr": false,
+            "ExposedPorts": {
+                "443/tcp": {},
+                "80/tcp": {}
+            },
+            "Tty": false,
+            "OpenStdin": false,
+            "StdinOnce": false,
+            "Env": [
+                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+                "NGINX_VERSION=1.11.10-1~jessie"
+            ],
+            "Cmd": [
+                "nginx",
+                "-g",
+                "daemon off;"
+            ],
+            "Healthcheck": {
+                "Test": [
+                    "CMD-SHELL",
+                    "echo 'hallo'"
+                ]
+            },
+            "Image": "nginx",
+            "Volumes": null,
+            "WorkingDir": "",
+            "Entrypoint": null,
+            "OnBuild": null,
+            "Labels": {
+                "bigboat.agent.url": "http://localhost:8080",
+                "bigboat.application.name": "nginx-healtcheck",
+                "bigboat.application.version": "latest",
+                "bigboat.domain": "innovation",
+                "bigboat.instance.name": "nginx-healtcheck",
+                "bigboat.service.name": "www",
+                "bigboat.service.type": "service",
+                "bigboat.startedBy": "qxy2qftqXnomawNjb",
+                "bigboat.storage.bucket": "nginx-healtcheck",
+                "bigboat.tld": "ictu",
+                "com.docker.compose.config-hash": "5ad5138394d4caf7077afa84572bed54c7c102ea71fd36860d716907d4f7bc4b",
+                "com.docker.compose.container-number": "1",
+                "com.docker.compose.oneoff": "False",
+                "com.docker.compose.project": "innovationnginxhealtcheck",
+                "com.docker.compose.service": "www",
+                "com.docker.compose.version": "1.11.0"
+            }
+        },
+        "NetworkSettings": {
+            "Bridge": "",
+            "SandboxID": "",
+            "HairpinMode": false,
+            "LinkLocalIPv6Address": "",
+            "LinkLocalIPv6PrefixLen": 0,
+            "Ports": null,
+            "SandboxKey": "",
+            "SecondaryIPAddresses": null,
+            "SecondaryIPv6Addresses": null,
+            "EndpointID": "",
+            "Gateway": "",
+            "GlobalIPv6Address": "",
+            "GlobalIPv6PrefixLen": 0,
+            "IPAddress": "",
+            "IPPrefixLen": 0,
+            "IPv6Gateway": "",
+            "MacAddress": "",
+            "Networks": null
+        }
+    }
+]
 ###


Even better ordering

Even better ordering

--- resources/assets/coffee/_classes/line-chart.coffee
+++ resources/assets/coffee/_classes/line-chart.coffee
@@ -176,9 +176,11 @@

   resize: =>
     @setDimensions()
+
+    @setScalesRange()
+
     @setSvgSize()
     @setWrapperSize()
-    @setScalesRange()
     @setAxesSize()
     @setLineSize()


exclude hidden files from output

exclude hidden files from output

express static server doesn't serve them and rejects with 404

--- services/clsi/app/coffee/OutputCacheManager.coffee
+++ services/clsi/app/coffee/OutputCacheManager.coffee
@@ -63,6 +63,11 @@
 				# copy all the output files into the new cache directory
 				results = []
 				async.mapSeries outputFiles, (file, cb) ->
+					# don't send dot files as output, express doesn't serve them
+					if file?.path?.match(/^\.|\/./)
+						logger.warn compileDir: compileDir, path: file.path, "ignoring dotfile in output"
+						return cb()
+					# copy other files into cache directory if valid
 					newFile = _.clone(file)
 					[src, dst] = [Path.join(compileDir, file.path), Path.join(cacheDir, file.path)]
 					OutputCacheManager._checkFileIsSafe src, (err, isSafe) ->


make s3 configurable via env vars

make s3 configurable via env vars

--- services/filestore/config/settings.defaults.coffee
+++ services/filestore/config/settings.defaults.coffee
@@ -1,6 +1,6 @@
 Path = require "path"

-module.exports =
+settings =
 	internal:
 		filestore:
 			port: 3009
@@ -11,22 +11,26 @@
 		# Choices are
 		# s3 - Amazon S3
 		# fs - local filesystem
-		backend: "fs"
-		stores:
-		  	# where to store user and template binary files
-			#
-			# For Amazon S3 this is the bucket name to store binary files in.
-			#
-			# For local filesystem this is the directory to store the files in.
-			# Must contain full path, e.g. "/var/lib/sharelatex/data".
-			# This path must exist, not be tmpfs and be writable to by the user sharelatex is run as.
-			user_files: Path.resolve(__dirname + "/../user_files")
-			public_files: Path.resolve(__dirname + "/../public_files")
-			template_files: Path.resolve(__dirname + "/../template_files")
-		# if you are using S3, then fill in your S3 details below
-		# s3:
-		# 	key: ""
-		# 	secret: ""
+		if process.env['AWS_KEY']?
+			backend: "s3"
+			s3:
+				key: process.env['AWS_KEY']
+				secret: process.env['AWS_SECRET']
+			stores:
+				user_files: process.env['AWS_S3_USER_FILES_BUCKET_NAME']
+				template_files: process.env['AWS_S3_TEMPLATE_FILES_BUCKET_NAME']
+				public_files: process.env['AWS_S3_PUBLIC_FILES_BUCKET_NAME']
+		else
+			backend: "fs"
+			stores:
+				#
+				# For local filesystem this is the directory to store the files in.
+				# Must contain full path, e.g. "/var/lib/sharelatex/data".
+				# This path must exist, not be tmpfs and be writable to by the user sharelatex is run as.
+				user_files: Path.resolve(__dirname + "/../user_files")
+				public_files: Path.resolve(__dirname + "/../public_files")
+				template_files: Path.resolve(__dirname + "/../template_files")
+

 	path:
 		uploadFolder: Path.resolve(__dirname + "/../uploads")
@@ -35,9 +39,15 @@
 		# Any commands to wrap the convert utility in, for example ["nice"], or ["firejail", "--profile=/etc/firejail/convert.profile"]
 		convertCommandPrefix: []

-	# Filestore health check
-	# ----------------------
-	# Project and file details to check in persistor when calling /health_check
-	# health_check:
-	# 	project_id: ""
-	# 	file_id: ""
+
+# Filestore health check
+# ----------------------
+# Project and file details to check in persistor when calling /health_check
+if process.env['HEALTH_CHECK_PROJECT_ID']? and process.env['HEALTH_CHECK_FILE_ID']?
+	settings.health_check =
+		project_id: process.env['HEALTH_CHECK_PROJECT_ID']
+		file_id: process.env['HEALTH_CHECK_FILE_ID']
+
+module.exports = settings
+
+console.log module.exports


recurly post endpoint

recurly post endpoint

--- server/code/index.coffee
+++ server/code/index.coffee
@@ -15,6 +15,7 @@
 flash = require 'connect-flash'
 eco = require 'eco'
 checkIdent = require 'ident-express'
+request = require 'request'

 {User} = require 'model/user'
 {Dataset} = require 'model/dataset'
@@ -242,6 +243,12 @@
           console.warn err
           return resp.send 500, error: 'Error trying to update status'
         return resp.send 200, status: 'ok'
+
+app.post '/recurly/?', (req, resp) ->
+  token = req.body.params.recurly_token
+  request.get "https://api.recurly.com/v2/recurly_js/result/#{token}", (err, resp, body) ->
+    console.log err, body
+

 ############ AUTHENTICATED ############
 app.all '*', ensureAuthenticated


remove log

remove log

--- bokehjs/src/coffee/renderer/glyph/square.coffee
+++ bokehjs/src/coffee/renderer/glyph/square.coffee
@@ -14,7 +14,6 @@
       @sw = @distance_vector('x', 'size', 'center')

     _render: (ctx, glyph_props, use_selection) ->
-      console.log "FOO"
       for i in @mask

         if isNaN(@sx[i] + @sy[i] + @sw[i])


Fix a possible null error

Fix a possible null error

--- app/assets/javascripts/controllers/toolbar.controller.js.coffee
+++ app/assets/javascripts/controllers/toolbar.controller.js.coffee
@@ -45,7 +45,11 @@
   ).property('@each.isActive')

   searchString: Ember.computed('queryString', ->
-    return decodeURIComponent(this.get('queryString').replace(/\+/g, ' '))
+    queryString = this.get('queryString')
+    if queryString?
+      return decodeURIComponent(queryString.replace(/\+/g, ' '))
+    else
+      return queryString
   )

   activeStatusFilters: ((key, activeStatusFilters)->


[improvement] Indicated when an alert's deadline is past on the Alerts view (2m)

--- app/assets/javascripts/houston/alerts/handlebars_helpers.coffee
+++ app/assets/javascripts/houston/alerts/handlebars_helpers.coffee
@@ -14,4 +14,5 @@

 Handlebars.registerHelper 'formatAlertDeadline', (deadline)->
   deadline = new Date(Date.parse(deadline)) if _.isString(deadline)
+  return "Past" if deadline < new Date()
   d3.time.format('%-I:%M %p%A')(deadline).replace /AM|PM/, (s)-> s.toLowerCase()


Only allow bundle bias if a file type was matched

--- src/grammar-registry.coffee
+++ src/grammar-registry.coffee
@@ -46,7 +46,7 @@
     contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath)

     score = @getGrammarPathScore(grammar, filePath)
-    if not grammar.bundledPackage
+    if score > 0 and not grammar.bundledPackage
       score += 0.25
     if @grammarMatchesContents(grammar, contents)
       score += 0.125


optimize parser

optimize parser

--- src/parser/parseExpression.coffee
+++ src/parser/parseExpression.coffee
@@ -211,58 +211,10 @@

         # build
         splitVariable = (variable) ->
-            if variable.indexOf('.') < 0 and variable.indexOf('[') < 0
-                return {
-                    result: [variable]
-                    elvis: [variable]
-                    name: variable
-                }
-            result = []
-            elvisResult = []
-            full = ''
-            i = 0
-            name = ''
-            elvisName = ''
-            commit = (end) ->
-                if name[name.length-1] is '?'
-                    name = name.substring 0, name.length-1
-                    isElvis = true
-                if name
-                    if name[0] is '['
-                        elvisName += name
-                    else
-                        if elvisName
-                            elvisName += '.' + name
-                        else
-                            elvisName = name
-                    result.push name
-                    name = ''
-                if elvisName
-                    if isElvis
-                        elvisResult.push elvisName
-                        elvisName = ''
-                    else if end
-                        elvisResult.push elvisName
-
-            while i < variable.length
-                a = variable[i++]
-                if a isnt '?'
-                    full += a
-                if a is '.'
-                    commit()
-                    continue
-                if a is '['
-                    commit()
-                if a is ']'
-                    name += a
-                    commit()
-                    continue
-                name += a
-            commit true
-
-            result: result
-            elvis: elvisResult
-            name: full
+            parts = variable.split(/[\.\[\(\?]/)
+
+            count: parts.length
+            firstPart: parts[0]

         toElvis = (name, isReserved) ->
             if isReserved
@@ -314,20 +266,19 @@
                 if d.type is 'key'
                     if d.assignment
                         sv = splitVariable d.value
-                        if sv.result[0] is 'this'
+                        if sv.firstPart is 'this'
                             name = '$$scope' + d.value.substring 4
-                        else if sv.result.length < 2
+                        else if sv.count < 2
                             name = '($$scope.$root || $$scope).' + d.value
                         else
                             name = '$$scope.' + d.value
                         ret.isSimple = false
                     else
-                        name = convert d.value
-                        firstPart = getFirstPart d.value
-                        if not reserved[firstPart]
+                        if reserved[d.value]
+                            name = d.value
+                        else
+                            name = convert d.value
                             ret.simpleVariables.push name
-                        if d.function
-                            ret.isSimple = false
                     if d.children.length
                         for c in d.children
                             key = "####{c.uniq}###"


JDomain: First try to resolve requested domain then resolve kd.io if needed

--- workers/social/lib/social/models/domain.coffee
+++ workers/social/lib/social/models/domain.coffee
@@ -213,15 +213,18 @@
     return {type: 'internal', slug, prefix, domain}

   resolveDomain = (domainData, callback)->
+
     return callback null  unless domainData.domainType

     {domain} = domainData

     dns = require 'dns'
-    dns.resolve 'kd.io', (err, baseIps)->
-      return callback err  if err
-
-      dns.resolve domain, (err, remoteIps)->
+    dns.resolve domain, (err, remoteIps)->
+      return callback new KodingError \
+        "Cannot resolve #{domain}", "RESOLVEFAILED"  if err
+
+      baseDomain = 'kd.io'
+      dns.resolve baseDomain, (err, baseIps)->
         return callback err  if err

         intersection = (a, b)->
@@ -231,7 +234,7 @@
         if (intersection baseIps, remoteIps).length > 0
           return callback null

-        callback new KodingError "CNAME settings are wrong.", "CNAMEMISMATCH"
+        callback new KodingError "CNAME record for #{domain} is not matching with #{baseDomain}", "CNAMEMISMATCH"

   createDomain = (domainData, account, callback)->


let the user remove his/her own domain

let the user remove his/her own domain

--- workers/social/lib/social/models/domain.coffee
+++ workers/social/lib/social/models/domain.coffee
@@ -35,6 +35,7 @@
                        'updateProxyRule', 'deleteProxyRule', 'setDomainCNameToProxyDomain',
                        'updateRuleOrders', 'fetchProxyRules', 'fetchProxyRulesWithMatches',
                        'fetchDNSRecords', 'createDNSRecord', 'deleteDNSRecord', 'updateDNSRecord'
+                       'remove'
                       ]
       static        : ['one', 'isDomainAvailable', 'registerDomain', 'createDomain']

@@ -373,3 +374,11 @@

         callback err, response

+  remove$: permit
+      advanced: [
+        { permission: 'delete own domains', validateWith: Validators.own }
+      ]
+      success: (client, callback)->
+        {delegate} = client.connection
+        @remove (err)=>
+          callback err


missed core

missed core

--- resources/assets/coffee/react/_components/comments-manager.coffee
+++ resources/assets/coffee/react/_components/comments-manager.coffee
@@ -149,7 +149,7 @@

       runInAction () ->
         uiState.comments.currentSort = data.sort
-        dataStore.updateWithCommentBundleJSON data
+        core.dataStore.updateWithCommentBundleJSON data

       @setState
         userFollow: data.user_follow


tests(queryOption): add test for query option

--- src/spec/fetchstock.spec.coffee
+++ src/spec/fetchstock.spec.coffee
@@ -33,6 +33,17 @@
       done()
     .catch done

+  it 'should apply the query string to the query', ->
+
+    @fetchStocks.queryString = "sku=\"123\""
+    spyOn(@fetchStocks.client.inventoryEntries, 'where').andReturn({fetch: ->})
+
+    @fetchStocks.run()
+
+    expect(@fetchStocks.client.inventoryEntries.where)
+    .toHaveBeenCalledWith(@fetchStocks.queryString)
+    @fetchStocks.queryString = ""
+
   it 'should get the stock Information for a specific channel only', (done) ->
     spyOn(@fetchStocks, '_getChannelId').andCallFake -> Promise.resolve '123'
     spyOn(@fetchStocks.client.inventoryEntries, 'fetch').andCallFake -> Promise.resolve SpecHelper.singleStockMock()


onbeforeunload callback taken to its own method

--- client/Framework/core/KDWindowController.coffee
+++ client/Framework/core/KDWindowController.coffee
@@ -105,42 +105,47 @@
     , yes

     # unless window.location.hostname is 'localhost'
-    window.addEventListener 'beforeunload', (event) =>
-      # fixme: fix this with appmanager
-
-      if @getSingleton('mainView')?.mainTabView?.panes
-        for pane in @getSingleton('mainView').mainTabView.panes
-          msg = no
-
-          # For open Tabs (apps, editors)
-          if pane.getOptions().type is "application" and pane.getOptions().name isnt "New Tab"
-            msg = "Please make sure that you saved all your work."
-
-          # This cssClass needs to be added to the KDInputView OR
-          # to the wrapper (for ace)
-          pane.data.$(".warn-on-unsaved-data").each (i,element) =>
-
-            checkForUnsavedData = (el)=>
-              foundUnsavedData = no
-
-              # ACE-specific content check
-              $(el).find(".ace_editor div.ace_line > span").each (i,e)=>
-                if $(e).text()
-                  foundUnsavedData = yes
-
-              foundUnsavedData
-
-            # If the View is a KDInputview, we don"t need to look
-            # further than the .val(). For ACE and others, we call
-            # the checkForUnsavedData function above
-            if ($(element).hasClass("kdinput") and $(element).val() or checkForUnsavedData(element))
-              msg = "You may lose some input that you filled in."
-
-
-      if msg # has to be created in the above checks
-        event or= window.event
-        event.returnValue = msg if event # For IE and Firefox prior to version 4
-        return msg
+    window.addEventListener 'beforeunload', @bound "beforeUnload"
+
+  beforeUnload:(event)->
+
+    # fixme: fix this with appmanager
+
+    if @getSingleton('mainView')?.mainTabView?.panes
+      for pane in @getSingleton('mainView').mainTabView.panes
+        msg = no
+
+        # For open Tabs (apps, editors)
+        if pane.getOptions().type is "application" and pane.getOptions().name isnt "New Tab"
+          msg = "Please make sure that you saved all your work."
+
+        # This cssClass needs to be added to the KDInputView OR
+        # to the wrapper (for ace)
+        pane.data.$(".warn-on-unsaved-data").each (i,element) =>
+
+          checkForUnsavedData = (el)=>
+            foundUnsavedData = no
+
+            # ACE-specific content check
+            $(el).find(".ace_editor div.ace_line > span").each (i,e)=>
+              if $(e).text()
+                foundUnsavedData = yes
+
+            foundUnsavedData
+
+          # If the View is a KDInputview, we don"t need to look
+          # further than the .val(). For ACE and others, we call
+          # the checkForUnsavedData function above
+          if ($(element).hasClass("kdinput") and $(element).val() or checkForUnsavedData(element))
+            msg = "You may lose some input that you filled in."
+
+
+    if msg # has to be created in the above checks
+      event or= window.event
+      event.returnValue = msg if event # For IE and Firefox prior to version 4
+      return msg
+
+

   setDragInAction:(action = no)->


activity: define `MessagePane::lazyLoad` method

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -75,6 +75,19 @@
     else appManager.tell 'Activity', 'fetch', options, callback

+  lazyLoad: ->
+
+    {appManager} = KD.singletons
+    last         = @listController.getItemsOrdered().last
+    from         = last.getData().meta.createdAt.toISOString()
+
+    @fetch {from}, (err, items = []) =>
+
+      return KD.showError err  if err
+
+      items.forEach @lazyBound 'appendMessage'
+
+
   refresh: ->

     document.body.scrollTop            = 0


Activity: message pane put message implementation

--- client/Social/Activity/views/messagepane.coffee
+++ client/Social/Activity/views/messagepane.coffee
@@ -81,11 +81,7 @@
     @createFakeItemView value, clientRequestId

-  putMessage: (message) ->
-
-    {lastToFirst} = @getOptions()
-    index = if lastToFirst then @listController.getItemCount() else 0
-    @appendMessage message, index
+  putMessage: (message, index = 0) -> @appendMessage message, index

   createFakeItemView: (value, clientRequestId) ->


activity/flux: update unlike message action

--- client/activity/lib/flux/actions/message.coffee
+++ client/activity/lib/flux/actions/message.coffee
@@ -148,20 +148,17 @@
     UNLIKE_MESSAGE_FAIL
     UNLIKE_MESSAGE_SUCCESS } = actionTypes

-  dispatch UNLIKE_MESSAGE_BEGIN, { messageId }
+  userId = whoami()._id
+
+  dispatch UNLIKE_MESSAGE_BEGIN, { messageId, userId }

   socialapi.message.unlike { id: messageId }, (err, message) ->
     if err
-      dispatch UNLIKE_MESSAGE_FAIL, {
-        err, messageId
-      }
-      return
-
-    kd.utils.wait 573, ->
-      socialapi.message.byId {id: messageId}, (err, message) ->
-        dispatch UNLIKE_MESSAGE_SUCCESS, {
-          message
-        }
+      dispatch UNLIKE_MESSAGE_FAIL, { err, messageId }
+      return
+
+    dispatch UNLIKE_MESSAGE_SUCCESS, { messageId, userId }
+

 ###*
  * Edit message.


Client: Activity: Updated loadMessages action to dispatch SET_LOADED_WITH_SCROLL event

--- client/activity/lib/flux/actions/message.coffee
+++ client/activity/lib/flux/actions/message.coffee
@@ -21,7 +21,8 @@
   { socialapi } = kd.singletons
   { LOAD_MESSAGES_BEGIN, LOAD_MESSAGES_FAIL,
     LOAD_MESSAGES_SUCCESS, LOAD_MESSAGE_SUCCESS
-    SET_ALL_MESSAGES_LOADED, UNSET_ALL_MESSAGES_LOADED } = actionTypes
+    SET_ALL_MESSAGES_LOADED, UNSET_ALL_MESSAGES_LOADED
+    SET_LOADED_WITH_SCROLL, UNSET_LOADED_WITH_SCROLL } = actionTypes

   dispatch LOAD_MESSAGES_BEGIN, { channelId, options }

@@ -45,6 +46,9 @@
         messages.forEach (message) ->
           dispatchLoadMessageSuccess channelId, message
         dispatch LOAD_MESSAGES_SUCCESS, { channelId, messages }
+
+        if options.loadedWithScroll
+          dispatch SET_LOADED_WITH_SCROLL, { channelId }

       resolve { messages }


instructions book returning back to first page fixed

--- client/landing/app/MainApp/book/bookview.coffee
+++ client/landing/app/MainApp/book/bookview.coffee
@@ -110,7 +110,7 @@
   fillPage:(index)->

     cachePage index+1
-    index or= BookView.lastIndex
+    index ?= BookView.lastIndex
     BookView.lastIndex = index
     page = @getPage index


Add `recentV1TemplateIdsActivity` proxy to AnalyticRouter

--- services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee
+++ services/web/app/coffee/Features/Analytics/AnalyticsRouter.coffee
@@ -16,3 +16,7 @@
 		publicApiRouter.use '/analytics/recentTeamActivity',
 			AuthenticationController.httpAuth,
 			AnalyticsProxy.call('/recentTeamActivity')
+
+		publicApiRouter.use '/analytics/recentV1TemplateIdsActivity',
+			AuthenticationController.httpAuth,
+			AnalyticsProxy.call('/recentV1TemplateIdsActivity')


include start method for spinner

include start method for spinner

--- services/web/public/coffee/ide/pdfng/directives/pdfSpinner.coffee
+++ services/web/public/coffee/ide/pdfng/directives/pdfSpinner.coffee
@@ -8,13 +8,14 @@
 			constructor: () ->
 				# handler for spinners

-			add: (element) ->
-				h = element.height()
-				w = element.width()
+			add: (element, options) ->
 				size = 64
-				spinner = $('

')
+				spinner = $('

')
 				spinner.css({'font-size' : size + 'px'})
 				element.append(spinner)
+
+			start: (element) ->
+				element.find('.fa-spinner').addClass('fa-spin')

 			stop: (element) ->
 				element.find('.fa-spinner').removeClass('fa-spin')


operations: posts & subscriptions accessModel=authorize in defaultConfiguration

Fixed Github issue #47

--- src/local/operations.coffee
+++ src/local/operations.coffee
@@ -19,7 +19,7 @@
         title: "#{user} Channel Posts"
         description: "A buddycloud channel"
         channelType: "personal"
-        accessModel: "open"
+        accessModel: "authorize"
         publishModel: "publishers"
         defaultAffiliation: "publisher"
     status:
@@ -49,7 +49,7 @@
     subscriptions:
         title: "#{user} Subscriptions"
         description: "Browse my interests"
-        accessModel: "open"
+        accessModel: "authorize"
         publishModel: "publishers"
         defaultAffiliation: "member"


Throw an Error when an initialize function can not be found

--- tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/pageinit.coffee
+++ tapestry-core/src/main/coffeescript/META-INF/modules/t5/core/pageinit.coffee
@@ -95,6 +95,9 @@
             return

         fn = if functionName? then moduleLib[functionName] else moduleLib
+
+        unless fn?
+          throw new Error "Could not locate function `#{qualifiedName}'."

         if console.debugEnabled
           argsString = _.map(initArguments, JSON.stringify).join(", ")


change a default to local quorum; full quorum only makes sense with one datacenter

--- salvus/cassandra.coffee
+++ salvus/cassandra.coffee
@@ -68,7 +68,7 @@
     i = CONSISTENCIES.indexOf(consistency)
     if i == -1
         # unknown -- ?
-        return cql.types.consistencies.quorum # official default
+        return cql.types.consistencies.localQuorum
     else
         return CONSISTENCIES[Math.min(CONSISTENCIES.length-1,i+1)]

@@ -389,7 +389,7 @@
         @query_max_retry = opts.query_max_retry

         if opts.hosts.length == 1
-            # the default QUORUM won't work if there is only one node.
+            # the default consistency won't work if there is only one node.
             opts.consistency = 1

         @consistency = opts.consistency  # the default consistency (for now)


cassandra: put back reconnect() on timeout code

--- salvus/cassandra.coffee
+++ salvus/cassandra.coffee
@@ -409,6 +409,14 @@
         @_opts = opts
         @connect()

+    reconnect: (cb) =>
+        if not @conn?
+            @connect(cb)
+            return
+        @conn.shutdown? () =>
+            delete @conn
+            @connect(cb)
+
     connect: (cb) =>
         winston.debug("connect: connecting to the database server")
         console.log("connecting...")
@@ -693,6 +701,7 @@
                 winston.error(m)
                 c?(m)
                 c = undefined # ensure only called once
+                @reconnect()
             _timer = setTimeout(failed, 1000*@query_timeout_s)
             g (err) =>
                 clearTimeout(_timer)


handle case where offset changes during scroll (e.g. after scrolling past the top bar)

--- src/main/coffee/main.coffee
+++ src/main/coffee/main.coffee
@@ -249,7 +249,11 @@
                 $(window).scroll(objSelf._onScrollFn)
             $('html, body').animate({
                 scrollTop: newArticleOffset.top - $("#top-nav-bar").height() - 10
-            }, 500, "swing", () -> setTimeout(doneFn, 0))
+            }, 500, "swing", () ->
+                if newArticleOFfset.top != $("a[name='article" + newArticleId + "']").offset().top
+                    $('html, body').animate({
+                        scrollTop: $("a[name='article" + newArticleId + "']").offset().top - $("#top-nav-bar").height() - 10
+                    }, 500, "swing", () -> doneFn))
             e.preventDefault()

     _handleFeedKeys: (e) =>
@@ -313,7 +317,11 @@
                $(window).scroll(objSelf._onScrollFn)
            $('html, body').animate({
                 scrollTop: newArticleOffset.top - $("#top-nav-bar").height() - 10
-           }, 500, "swing", () -> setTimeout(doneFn, 0))
+            }, 500, "swing", () ->
+                if newArticleOFfset.top != $("a[name='article" + newArticleId + "']").offset().top
+                    $('html, body').animate({
+                        scrollTop: $("a[name='article" + newArticleId + "']").offset().top - $("#top-nav-bar").height() - 10
+                    }, 500, "swing", () -> doneFn))

     _initializeKeyboardNavigation: ->
         # Set up keyboard navigation


Server: CreateTeam: use generateAPIError in validateGroupData helper

--- servers/lib/server/handlers/createteam.coffee
+++ servers/lib/server/handlers/createteam.coffee
@@ -157,9 +157,8 @@
     client.sessionToken        = token
     client.connection.delegate = result.account

-    if validationError = validateGroupDataAndReturnError body
-      { statusCode, errorMessage } = validationError
-      return res.status(statusCode).send errorMessage
+    if validationError = validateGroupData body
+      return res.status(validationError.status).send validationError.message

     afterGroupCreate = afterGroupCreateKallback res, {
       body             : body
@@ -218,18 +217,18 @@
       return res.status(200).send data

-validateGroupDataAndReturnError = (body) ->
+validateGroupData = (body) ->

   unless body.slug
-    return { statusCode : 400, errorMessage : 'Group slug can not be empty.' }
+    return generateAPIError 400, 'Group slug can not be empty.'

   unless validateTeamDomain body.slug
-    return { statusCode : 400, errorMessage : 'Invalid group slug.' }
+    return generateAPIError 400, 'Invalid group slug.'

   else unless body.companyName
-    return { statusCode : 400, errorMessage : 'Company name can not be empty.' }
-
-  else null
+    return generateAPIError 400, 'Company name can not be empty.'
+
+  else return null

 # convertToArray converts given comma separated string value into cleaned,
 # trimmed, lowercased, unified array of string


removed useless options

removed useless options

--- tasks/lib/file_finder.coffee
+++ tasks/lib/file_finder.coffee
@@ -12,8 +12,8 @@
     options.src  = grunt.file.read(filepath)
     options.dest = helper.pathToFolder(filepath)

-    grunt.log.writeln "Files: ", filepath, this.requiredFiles( 'file', options )
-    grunt.log.writeln "Dirs : ", filepath, this.requiredFiles( 'dirs', options )
+    grunt.log.writeln "Files: ", filepath, this.requiredFiles('file', options )
+    grunt.log.writeln "Dirs : ", filepath, this.requiredFiles('dirs', options )

   exports.requiredFiles = (key, options) ->
     keyword = if key == "file" then options.keyword else options.dirKeyword
@@ -23,11 +23,11 @@
     re = helper.searchRegexp(keyword)
     listOfFiles = []
     for line in options.src.split('\n')
-      listOfFiles.push line.replace re, options.dest + "$1" + this.extension(key, options.extension) if line.match(re)
+      listOfFiles.push
+        line.replace
+          re
+          options.dest + "$1" + helper.extension(key, options.extension) if line.match(re)

     listOfFiles

-  exports.extension = (key, extension) ->
-    if key == "file" then extension else "*"+extension
-
   exports


Proper data hiding in the Source class

Proper data hiding in the Source class

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -967,30 +967,31 @@

     class Source
       constructor: (s) ->
-        @obs = s
-        @queue = []
-        @isEnded = false
+        queue = []
+        isEnded = false
+        @subscribe = s.subscribe
+        @markEnded = -> isEnded = true
         if s instanceof Property
-          @value = ()  -> @queue[0]
-          @push  = (x) -> @queue = [x]
-          @ended = ()  -> false
-          @hasAtLeast = (c) -> @queue.length
+          @consume = () -> queue[0]
+          @push  = (x) -> queue = [x]
+          @mayHave = -> true
+          @hasAtLeast = (c) -> queue.length
         else
-          @value = ()  -> @queue.shift()
-          @push  = (x) -> @queue.push(x)
-          @ended = (c) -> @queue.length < c && @isEnded
-          @hasAtLeast = (c) -> @queue.length >= c
+          @consume = () -> queue.shift()
+          @push  = (x) -> queue.push(x)
+          @mayHave = (c) -> !isEnded || queue.length >= c
+          @hasAtLeast = (c) -> queue.length >= c
     sources = _.map ((s) -> new Source(s)), sources

     new EventStream (sink) ->
       match = (p) ->
         _.all(p.ixs, (i) -> sources[i.index].hasAtLeast(i.count))
       cannotMatch = (p) ->
-        _.any(p.ixs, (i) -> sources[i.index].ended(i.count))
+        _.any(p.ixs, (i) -> !sources[i.index].mayHave(i.count))
       part = (source, sourceIndex) -> (unsubAll) ->
-        source.obs.subscribe (e) ->
+        source.subscribe (e) ->
           if e.isEnd()
-            sources[sourceIndex].isEnded = true
+            sources[sourceIndex].markEnded()
             if _.all(pats, cannotMatch)
               reply = Bacon.noMore
               sink end()
@@ -1000,7 +1001,7 @@
             sources[sourceIndex].push e.value()
             for p in pats
                if match(p)
-                 val = p.f(sources[i.index].value() for i in p.ixs ...)
+                 val = p.f(sources[i.index].consume() for i in p.ixs ...)
                  reply = sink next(val)
                  break
           unsubAll() if reply == Bacon.noMore


step 3

step 3

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -1078,15 +1078,15 @@
     @args = args
     @flatDeps = null

-    that = @
-
-    dependsOn = (b) ->
-      if !that.flatDeps?
-        that.flatDeps = {}
-        that.collectDeps this
-      return that.flatDeps[b.id]
-
     @apply = (obs) ->
+      that = @
+
+      dependsOn = (b) ->
+        if !that.flatDeps?
+          that.flatDeps = {}
+          that.collectDeps this
+        return that.flatDeps[b.id]
+
       deps = _.cached (=> @findDeps([@context].concat(@args)))
       obs.internalDeps = obs.internalDeps || deps
       obs.dependsOn = dependsOn


remove dead code

remove dead code

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -894,12 +894,6 @@
     unsubscribeFromSource = nop
     removeSub = (subscription) ->
       subscriptions = _.without(subscription, subscriptions)
-    waiters = null
-    done = ->
-      if waiters?
-        ws = waiters
-        waiters = null
-        w() for w in ws
     pushIt = (event) ->
       if not pushing
         return if event is prevError
@@ -919,7 +913,6 @@
         while queue.length
           event = queue.shift()
           @push event
-        done(event)
         if @hasSubscribers()
           Bacon.more
         else


implemented empty-groups command

implemented empty-groups command

--- coffee_lib/crowdutil/subcmd/empty-groups.coffee
+++ coffee_lib/crowdutil/subcmd/empty-groups.coffee
@@ -35,6 +35,28 @@
       console.log res
       # TODO: now remove each member from the group
       # ignore each remove errors. only log.
+      # res [ 'uid1' , 'uid2' ]
+      async.each(res,
+        (user, uDone) ->
+          try
+            crhelp.rmUserFromGroup(crowd, user, group, (err) ->
+              if err
+                console.log err.message
+              else
+                console.log group + ' - ' + user
+              uDone() # ignore error
+            )
+          catch err
+            console.log err.message
+            uDone()
+          return
+        , (err) ->
+          if err
+            console.log err.meesage
+          else
+            console.log "DONE emptying " + group
+          return
+      )
     )
   catch err
     console.log err.message


file listing: special case of home directory as "" string for the memorized directory path

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -707,16 +707,17 @@
     make_path: ->
         v = []
         v.push <PathSegmentLink path='' display={<Icon name='home' />} key=0 actions={@props.actions} />
-        if @props.current_path == ""
-            return v
         path = @props.current_path
         root = path[0] == '/'
         if root
             path = path[1..]
-        path_segments = path.split('/')
+        if @props.current_path == ''
+            path_segments = []
+        else
+            path_segments = path.split('/')
         history_segments = @props.history_path.split('/')
         for segment, i in history_segments
-            is_last = i == path_segments.length - 1
+            is_current = i == path_segments.length - 1
             is_history = i >= path_segments.length
             v.push <PathSegmentLink
                     path      = {history_segments[..i].join('/')}
@@ -724,7 +725,7 @@
                     full_name = {segment}
                     key       = {i+1}
                     actions   = {@props.actions}
-                    active    = {is_last}
+                    active    = {is_current}
                     history   = {is_history} />
         return v


render %auto messages

render %auto messages

--- src/smc-webapp/sagews/output.cjsx
+++ src/smc-webapp/sagews/output.cjsx
@@ -22,6 +22,11 @@
     propTypes :
         output : rtypes.object.isRequired
         flags  : rtypes.string
+
+    render_auto: ->
+        # This is deprecated, but can be in some older worksheets.
+        # It should do nothing for static rendering.
+        return <span/>

     render_stdout: (value, key) ->
         <Stdout key={key} message={fromJS(text:value)} />


use stat instead of lstat to accept symbolic links

--- src/steroids/GruntDefaults.coffee
+++ src/steroids/GruntDefaults.coffee
@@ -42,7 +42,7 @@
   copyFilesSyncRecursive = (options)->
     grunt.verbose.writeln "Copying files from #{options.sourcePath} to #{options.destinationDir} using #{options.relativeDir} as basedir"

-    for filePath in grunt.file.expand options.sourcePath when fs.lstatSync(filePath).isFile()
+    for filePath in grunt.file.expand options.sourcePath when fs.statSync(filePath).isFile()
       grunt.verbose.writeln "Copying file #{filePath}"

       relativePath = path.relative options.relativeDir, filePath
@@ -121,7 +121,7 @@
     javascripts = []
     sourceFiles = grunt.file.expand Paths.application.compiles.models

-    for filePath in sourceFiles when fs.lstatSync(filePath).isFile()
+    for filePath in sourceFiles when fs.statSync(filePath).isFile()
       grunt.verbose.writeln "Compiling model file at #{filePath}"
       javascripts.push grunt.file.read(filePath, "utf8").toString()
       fs.unlinkSync filePath
@@ -209,7 +209,7 @@
     viewDirectories = []

     # get each view folder (except layout)
-    for dirPath in grunt.file.expand(path.join(appViewsDirectory, "*")) when fs.lstatSync(dirPath).isDirectory()
+    for dirPath in grunt.file.expand(path.join(appViewsDirectory, "*")) when fs.statSync(dirPath).isDirectory()
       basePath = path.basename(dirPath)
       unless basePath is "layouts" + path.sep or basePath is "layouts"
         viewDirectories.push dirPath
@@ -235,7 +235,7 @@

       applicationLayoutFile = grunt.file.read layoutFilePath, "utf8"

-      for filePathPart in grunt.file.expand(path.join(viewDir, "**", "*")) when fs.lstatSync(filePathPart).isFile()
+      for filePathPart in grunt.file.expand(path.join(viewDir, "**", "*")) when fs.statSync(filePathPart).isFile()

         filePath = path.resolve filePathPart

@@ -284,7 +284,7 @@
     mergesExist = fs.existsSync mergesDirectory
     if mergesExist
       # android
-      for filePath in grunt.file.expand(path.join(androidMergesDirectory, "*")) when fs.lstatSync(filePath).isFile()
+      for filePath in grunt.file.expand(path.join(androidMergesDirectory, "*")) when fs.statSync(filePath).isFile()
         filePath = path.normalize(filePath) # fix windows path syntax
         grunt.log.writeln colorize.ansify(" #yellow[Copying Android Merges:]")

@@ -304,7 +304,7 @@
         fs.writeFileSync filePathInDistWithAndroidExtensionPrefix, fs.readFileSync(filePath)

       # ios
-      for filePath in grunt.file.expand(path.join(iosMergesDirectory, "*")) when fs.lstatSync(filePath).isFile()
+      for filePath in grunt.file.expand(path.join(iosMergesDirectory, "*")) when fs.statSync(filePath).isFile()
         filePath = path.normalize(filePath) # fix windows path syntax
         grunt.log.writeln colorize.ansify(" #yellow[Copying iOS Merges:]")


Fixed: coffee lint errors were not printing to stdout

--- tasks/coffeeLint.coffee
+++ tasks/coffeeLint.coffee
@@ -20,6 +20,6 @@
       else
         grunt.log.header source.src[0]
         for error in errors
-          results.push grunt.log.error("#" + error.lineNumber
-          + ": " + error.message + " (" + error.context + ")")
+          results.push grunt.log.error("#" + error.lineNumber +
+          ": " + error.message + " (" + error.context + ")")
     return results


Removed consolelog

Removed consolelog

--- tasks/shortStats.coffee
+++ tasks/shortStats.coffee
@@ -34,7 +34,6 @@
           name: 'eloleaderboard'
           data: eloArray
         }
-        console.log eloArray, doc
         Statistics.update({name: 'eloleaderboard'}, doc, {upsert:true}, (err)->
           if err
             console.log err


スケジュールをhearしスケジュールを表示する

スケジュールをhearしスケジュールを表示する

--- scripts/schedule.coffee
+++ scripts/schedule.coffee
@@ -16,7 +16,7 @@
     id = Number(msg.match[1])
     storage.unregistParty(robot, id)

-  robot.hear /何時から|何時開催|何時開始/, (msg) ->
+  robot.hear /何時から|何時開催|何時開始|スケジュール/, (msg) ->
     showScheduleList(msg)

   showScheduleList = (msg) ->


Add ranges into health check

Add ranges into health check

--- app/coffee/HealthChecker.coffee
+++ app/coffee/HealthChecker.coffee
@@ -19,7 +19,7 @@
 		jobs = [
 			(cb)->
 				opts = getOpts()
-				opts.json = {lines: lines, version: 42}
+				opts.json = {lines: lines, version: 42, ranges: {}}
 				request.post(opts, cb)
 			(cb)->
 				opts = getOpts()


add explicit wait to test

add explicit wait to test

--- app/coffee/tests/browser.coffee
+++ app/coffee/tests/browser.coffee
@@ -968,7 +968,8 @@
       # log the form's POST target in case of random test failure
       formTarget = casper.getElementAttribute '#main_form', 'action'
       casper.log "main form will post to #{formTarget}", "debug"
-    casper.thenClick '#resign-button', -> # Click the resign button
+    casper.thenClick '#resign-button'  # Click the resign button
+    casper.waitUntilVisible '.confirm-resign-button', ->
       test.assertVisible '.confirm-resign-button'
       casper.thenClick '.confirm-resign-button'


Immediate delay is a prop

Immediate delay is a prop

--- app/components/auto-save.coffee
+++ app/components/auto-save.coffee
@@ -10,6 +10,7 @@
     className: 'auto-save'
     resource: null
     keyboardDelay: 10 * 1000
+    immediateDelay: 0
     resetDelay: 2 * 1000

   getInitialState: ->
@@ -41,14 +42,14 @@
     delay = if @isTextInput e.target
       @props.keyboardDelay
     else
-      0
+      @props.immediateDelay
     @saveResourceAfter delay

   handleClick: (e) ->
     if e.target.tagName is 'BUTTON'
       e.persist()
       console?.log 'AutoSave::handleClick', e
-      @saveResourceAfter 0
+      @saveResourceAfter @props.immediateDelay

   handleBlur: (e) ->
     if @isTextInput e.target
@@ -57,7 +58,7 @@
       delay = if @state.saving?
         @props.keyboardDelay
       else
-        0
+        @props.immediateDelay
       @saveResourceAfter delay

   saveResourceAfter: (delay) ->


Fix logout

Fix logout

--- app/controllers/settings.coffee
+++ app/controllers/settings.coffee
@@ -116,10 +116,10 @@
         @setupNotifications()
       , notifyTime - now

-  logout: ->
+  logout: (e) ->
+    if e? then Setting.set("offlineMode","true")
     Cookies.removeItem("uid")
     Cookies.removeItem("token")
-    Setting.set("offlineMode","true")
     document.location.reload()

   clearData: =>


Try to fetch subjects again when the server says to

--- app/pages/project/classify.cjsx
+++ app/pages/project/classify.cjsx
@@ -142,9 +142,20 @@
         workflow_id: workflow.id
         sort: 'cellect' unless SKIP_CELLECT

-      fetchSubjects = apiClient.type('subjects').get(subjectQuery).then (subjects) ->
-        nonLoadedSubjects = (newSubject for newSubject in subjects when newSubject isnt subjectToLoad)
-        upcomingSubjects.forWorkflow[workflow.id].push nonLoadedSubjects...
+      fetchSubjects = apiClient.type('subjects').get subjectQuery
+        .catch (error) ->
+          if error.message.indexOf('please try again') is -1
+            throw error
+          else
+            new Promise (resolve, reject) ->
+              fetchSubjectsAgain = ->
+                apiClient.type('subjects').get subjectQuery
+                  .then resolve
+                  .catch reject
+              setTimeout fetchSubjectsAgain, 2000
+        .then (subjects) ->
+          nonLoadedSubjects = (newSubject for newSubject in subjects when newSubject isnt subjectToLoad)
+          upcomingSubjects.forWorkflow[workflow.id].push nonLoadedSubjects...

       # If we're filling this list for the first time, we won't have a subject selected, so try again.
       subject ?= fetchSubjects.then ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/core_object.coffee
+++ test/core_object.coffee
@@ -53,7 +53,8 @@
   o = Core()

   o.include(M)
-
+  o.include(M)
+
 test "#include multiple", ->
   o = Core
     test: "my_val"


[fix] malformation of grunt.js

[fix] malformation of grunt.js

--- app/templates/_gruntfile.coffee
+++ app/templates/_gruntfile.coffee
@@ -76,8 +76,9 @@
         files:[
           '*.html'
           '**/*.html'
+        ]
+        tasks:
           'notify:html'
-        ]
       sass:
         files:'css/src/*.scss'
         tasks: [


updated grunt task to wathc new images

updated grunt task to wathc new images

--- app/templates/_gruntfile.coffee
+++ app/templates/_gruntfile.coffee
@@ -45,6 +45,12 @@
           "sass:server",
           "autoprefixer",
           "copy:server"
+        ]
+
+      images:
+        files: ["<%%= yeoman.app %>/images/*", "<%%= yeoman.app %>/fonts/*"]
+        tasks: [
+          "copy:images"
         ]

       # livereload:
@@ -216,9 +222,18 @@
           dot: true
           cwd: "<%%= yeoman.app %>"
           dest: "<%%= yeoman.dist %>"
-          src: ["*.{ico,png,txt}", ".htaccess", "images/{,*/}*.{png,jpg,jpeg,webp,gif,svg}", "fonts/*"]
+          src: ["*.{ico,png,txt}", ".htaccess"]
         ,
           "<%%= yeoman.dist %>/scripts/jquery.min.js": "<%%= yeoman.app %>/bower_components/jquery/jquery.min.js"
+        ]
+
+      images:
+        files: [
+          expand: true
+          dot: true
+          cwd: "<%%= yeoman.app %>"
+          dest: "<%%= yeoman.dist %>"
+          src: ["images/{,*/}*.{png,jpg,jpeg,webp,gif,svg}", "fonts/*"]
         ]

       server:


minor refactor in short beam sys

minor refactor in short beam sys

--- src/javascript/game/entity/samus/systems/short_beam_system.coffee
+++ src/javascript/game/entity/samus/systems/short_beam_system.coffee
@@ -8,7 +8,7 @@
   muzzleVelocity: 200/1000
   bulletLife: 50 / (200/1000)

-newBullet = (shortBeam, position,direction) ->
+newBullet = (weapon, position,direction) ->
   offsetX = GUN_SETTINGS.offsetX
   offsetY = GUN_SETTINGS.offsetY
   velocity = GUN_SETTINGS.muzzleVelocity
@@ -22,7 +22,7 @@

   return [
     Common.Bullet.merge
-      damage: shortBeam.get('damage')
+      damage: weapon.get('damage')
     Common.Visual.merge
       layer: 'creatures'
       spriteName: 'bullet'
@@ -92,11 +92,11 @@
       return 'ready'

   _cooldown: (ms) ->
-    @update @get('short_beam').set('cooldown',ms)
+    @setProp('short_beam','cooldown',ms)
     'coolDown'

   _fireBullet: ->
-    dir = @get('samus').get('direction')
+    dir = @getProp('samus','direction')
     shortBeam = @get('short_beam')
     pos = @get('position')
     @newEntity newBullet(shortBeam,pos,dir)


Added server error subscription to AuthModal

--- app/views/core/AuthModal.coffee
+++ app/views/core/AuthModal.coffee
@@ -18,6 +18,9 @@
     'click #gplus-login-btn': 'onClickGPlusLoginButton'
     'click #facebook-login-btn': 'onClickFacebookLoginButton'
     'click #close-modal': 'hide'
+
+  subscriptions:
+    'errors:server-error': 'onServerError'

   # Initialization


Fix UI bug

Fix UI bug

--- app/views/search/keys.js.coffee
+++ app/views/search/keys.js.coffee
@@ -55,14 +55,17 @@
     for locale in localeOrder
       do (locale) ->
         translation = (trans for trans in key.translations when trans.locale.rfc5646 == locale)[0]
-        klass = if translation.translated && translation.approved
-          'text-success'
-        else if translation.translated
-          'text-info'
-        else if translation.approved == false
-          'text-error'
+        if translation?
+          klass = if translation.translated && translation.approved
+            'text-success'
+          else if translation.translated
+            'text-info'
+          else if translation.approved == false
+            'text-error'
+          else
+            ''
+          td = $('<td/>').appendTo(tr)
+          $('<a/>').text(translation.copy[0..30]).attr('href', translation.url).
+              addClass(klass).appendTo(td)
         else
-          ''
-        td = $('<td/>').appendTo(tr)
-        $('<a/>').text(translation.copy[0..30]).attr('href', translation.url).
-        addClass(klass).appendTo(td)
+          td = $('<td/>').appendTo(tr)


Add overrideable methods for parsing the min/max in the range field stats

--- src/coffee/cilantro/ui/field/controls/range.coffee
+++ src/coffee/cilantro/ui/field/controls/range.coffee
@@ -52,6 +52,18 @@

             @set(@context)

+        # Method for parsing the value of the StatModel corresponding to the
+        # minimum viable value for this range. By default, this method simply
+        # returns the value itself.
+        parseMinStat: (value) ->
+            return value
+
+        # Method for parsing the value of the StatModel corresponding to the
+        # maximum viable value for this range. By default, this method simply
+        # returns the value itself.
+        parseMaxStat: (value) ->
+            return value
+
         # Tries to find and read the values of the stats models with the keys
         # 'min' and 'max' from this model's StatsCollection. After these values
         # are read, the bounds will be updated if they need to be so that the
@@ -62,14 +74,14 @@
             )?.get('value')

             if statsMin?
-                @minLowerBound = statsMin
+                @minLowerBound = @parseMinStat(statsMin)

             statsMax = _.find(@model.stats.models, (stat) ->
                 return stat.get('label') == 'Max'
             )?.get('value')

             if statsMax?
-                @maxUpperBound = statsMax
+                @maxUpperBound = @parseMaxStat(statsMax)

             @updateBounds()

@@ -86,9 +98,9 @@
         updateBounds: ->
             if @isClosed? and not @isClosed
                 if @ui.lowerBound.val() == ""
-                    @ui.lowerBound.val(@minLowerBound)
+                    @setLowerBoundValue(@minLowerBound)
                 if @ui.upperBound.val() == ""
-                    @ui.upperBound.val(@maxUpperBound)
+                    @setUpperBoundValue(@maxUpperBound)

         getField: ->
             return @model.id


Unbind factlinkCreated event before binding again to it

--- core/app/assets/javascripts/intermediate.coffee
+++ core/app/assets/javascripts/intermediate.coffee
@@ -29,6 +29,7 @@
       onFactlinkCreated = (e, id) ->
         remote.highlightNewFactlink text, id

+      $(document).off "factlinkCreated"
       $(document).on "factlinkCreated", (e, id, created_text) ->
         onFactlinkCreated(e, id) if created_text == text


temporarily disabled Node.js dev Express logging to cmd

it causes max buffer exceeded, and we are intested in api calls only
anyway

--- server/app/index.coffee
+++ server/app/index.coffee
@@ -16,7 +16,7 @@
     app.use express.favicon()

     if config.env.development
-      app.use express.logger 'dev'
+      # app.use express.logger 'dev'
       app.locals.pretty = true

     app.use express.bodyParser()


Take alts into account when sending updates.

--- server/bw/battle.coffee
+++ server/bw/battle.coffee
@@ -273,7 +273,7 @@

   # Tells every spectator something.
   tell: (args...) ->
-    spectatorIds = _.unique(@spectators.map((s) -> s.name))
+    spectatorIds = _.unique(@spectators.map((s) -> s.original?.name || s.name))
     @tellPlayer(spectatorId, args...)  for spectatorId in spectatorIds
     @log.push(args)
     true
@@ -884,10 +884,11 @@
   # Sends battle updates to each spectator.
   sendUpdates: ->
     for user in @spectators
-      queue = @queues[user.name]
+      userName = user.original?.name || user.name
+      queue = @queues[userName]
       continue  if !queue || queue.length == 0
       user.send('updateBattle', @id, queue)
-      delete @queues[user.name]
+      delete @queues[userName]

   cannedText: (type, args...) ->
     newArgs = []


Refactor: Remove unnecessary include and add TODO

--- server/bw/battle.coffee
+++ server/bw/battle.coffee
@@ -6,7 +6,6 @@
 {Weather} = require './weather'
 {Attachment, Attachments} = require './attachment'
 {Protocol} = require '../../shared/protocol'
-{Conditions} = require '../../shared/conditions'
 Query = require './queries'
 {EventEmitter} = require 'events'

@@ -39,6 +38,7 @@
     @numActive = attributes.numActive || 1

     # An array of conditions like clauses or team preview that this battle has.
+    # TODO: Remove
     @conditions = (attributes.conditions && _.clone(attributes.conditions))
     @conditions ||= []


fix file attachments on poll

fix file attachments on poll

--- vue/src/shared/models/poll_model.coffee
+++ vue/src/shared/models/poll_model.coffee
@@ -50,6 +50,8 @@
     pollOptionNames: []
     pollOptionIds: []
     customFields: {}
+    files: []
+    imageFiles: []
     # optionDate: moment().format('YYYY-MM-DD')

   audienceValues: ->


enhance user feedback

enhance user feedback

--- app/assets/javascripts/controllers/ssh_keys_controller.js.coffee
+++ app/assets/javascripts/controllers/ssh_keys_controller.js.coffee
@@ -10,6 +10,9 @@
       promise = $http.post('/api/users/ssh_keys', {'ssh_key':{'title':$scope.title,'content':$scope.content}})
       promise.success (data, status, headers, config) ->
         $scope.ssh_keys.push data
+        $scope.toggleNewSshKeyForm()
+        $scope.title = ""
+        $scope.content = ""
       promise.error (data, status, headers, config) ->
         $scope.all_errors = data.errors
     $scope.deleteSshKey = (index) ->


dialog 修复dimmer 覆盖弹出层的问题

dialog 修复dimmer 覆盖弹出层的问题

--- src/widget/layout/dialog.coffee
+++ src/widget/layout/dialog.coffee
@@ -176,8 +176,13 @@
 			})
 			if @_dimmerClose
 				$(_dimmerDom).on("click", ()=> @hide())
-			@_dom.parentNode.appendChild(_dimmerDom)
+			if cola.device.desktop
+				document.body.appendChild(_dimmerDom)
+			else
+				container = @_context or @_dom.parentNode
+				container.appendChild(_dimmerDom)
 			@_doms.modalLayer = _dimmerDom
+
 		$(_dimmerDom).css({
 			opacity: @get("modalOpacity")
 			zIndex: cola.floatWidget.zIndex()


cleanup logs and try to use user.name as fallback

--- src/alias.coffee
+++ src/alias.coffee
@@ -36,7 +36,6 @@
     '@' + val[1].split(',').join(' @')])
   # Convert 2D list to native object
   groupCache = _.object(staticGroups)
-  console.log groupCache
   return groupCache

 getGroups = (match) ->
@@ -48,7 +47,8 @@

 # Replace aliases with @mentions in the message
 expand = (message, user) ->
-  filterName = user.mention_name || user[user_prop]
+  # mention_name is for Hipchat
+  filterName = user[user_prop] || user.mention_name || user.name
   groups = getGroups(message.match)
   for own alias, members of groups
     # Filter inviduals from their own messages.
@@ -77,6 +77,5 @@
     return

   regex = buildRegExp()
-  console.log 'REGEX', regex
   robot.hear regex, (msg) ->
     msg.send expand(msg.message.text, msg.message.user)


fix var reference

fix var reference

--- src/alias.coffee
+++ src/alias.coffee
@@ -31,8 +31,7 @@
   # Convert 2D list to native object
   # expand "user1,user2" to "@user1 @user2"
   groups = _.reduce(groups, (obj, val, index) ->
-    sendTo = val[1]
-    sendTo = "@" + users.replace(/,/g, ' @')
+    sendTo = "@" + val[1].replace(/,/g, ' @')
     obj[val[0].toLowerCase()] = sendTo
     obj
   , {})


change include

change include

--- test/loggingSpec.coffee
+++ test/loggingSpec.coffee
@@ -1,6 +1,6 @@
 logging = require '../index'
 expect  = require('chai').expect
-sinon   = require 'sinon'
+spy     = require('sinon').spy

 describe 'ewg/logging', ->
   a    = 1
@@ -9,7 +9,7 @@

   describe '#log()', ->
     beforeEach ->
-      sinon.spy(logging.console.log, 'call')
+      spy(logging.console.log, 'call')

     afterEach ->
       logging.console.log.call.restore()


Remove bogus line break.

Remove bogus line break.

--- test/e2e/subjectDetailSpec.coffee
+++ test/e2e/subjectDetailSpec.coffee
@@ -322,8 +322,7 @@

         it 'should show the ethnicity', ->
           demographics.then (table) ->
-            table.ethnicity
-            .then (ethnicity) ->
+            table.ethnicity.then (ethnicity) ->
               expect(ethnicity, 'The ethnicity is missing')
                 .to.exist.and.to.not.be.empty


Updated node global module test

Updated node global module test

--- test/node/global.coffee
+++ test/node/global.coffee
@@ -1,5 +1,5 @@
 udefineFunction = require '../../dist/udefine'
-require '../../dist/global'
+udefineGlobal = require '../../dist/global'
 {expect} = require 'chai'

 describe 'udefine global function', ->
@@ -7,6 +7,9 @@
   it 'udefine is bound to global', ->
     expect(global.udefine).to.be.a('function')

+  it 'return value of required global udefine is undefined', ->
+    expect(udefineGlobal).to.be('undefined')
+
   it 'udefine can be called without global prefix', ->
     expect(udefine).to.be.a('function')


Removed debug code

Removed debug code

--- test/option_sets.coffee
+++ test/option_sets.coffee
@@ -23,5 +23,3 @@
   return results

 module.exports = if options.all then combos(OPTIONS) else [options]
-
-console.log module.exports


performance tweaks

performance tweaks

--- test/performance.coffee
+++ test/performance.coffee
@@ -24,7 +24,7 @@
     else text = '+' + ( perc-100 ) + '%'

     stats['diff'] = text
-    line = ' ' + title + ' x' + iterations + ' '
+    line = '    ' + title + ' x' + iterations + ' '
     line += ' ' while line.length < 58
     line += JSON.stringify( stats )
       .replace(/:"?([^"}]+)/g,': $1\t')
@@ -37,7 +37,7 @@

   after ->
     console.log()
-    console.log( ' ==== benchmark results ====' )
+    console.log( '  benchmark results' )
     console.log( output )

   func = (arg) -> return arg


Parse Problem format on error

Parse Problem format on error

--- src/basic.coffee
+++ src/basic.coffee
@@ -27,16 +27,13 @@

   # Utility functions --------------------------------------

-  # String -> {token: String, choice: String}
+  # JSON -> {token: String, choice: String}
   parseSuggestResponse: (content) ->
     { token: content.token, choice: content.choice }

-  # String -> {code: Number, message: String}
+  # JSON -> {code: Number, message: String}
   parseErrorResponse: (content) ->
-    parts = content.split(/[\r\n]+/)
-    code = parseInt(parts[0].replace(/ERROR: /, ""))
-    message = parts[1]
-    { code: code, message: message }
+    { code: content.subtype, messages: content.messages }


[#6] fixed selector specs

[#6] fixed selector specs

--- test/tourney/selector.spec.coffee
+++ test/tourney/selector.spec.coffee
@@ -11,7 +11,7 @@
       expect(selector(2).games).to.eq 1

     it 'returns object containing a schedule', ->
-      expect(selector(2).schedule).to.eql [[1,2]]
+      expect(selector(2).schedule).to.eql [{id:1,teams:[1,2]}]

   describe 'given number of teams greater than 8', ->
     it 'returns object with type "round robin"', ->


Overload the child process environment.

Overload the child process environment.

--- src/child.coffee
+++ src/child.coffee
@@ -17,6 +17,14 @@
     if options.env.IMPROMPTU_SHELL
       impromptu.options.prompt = options.env.IMPROMPTU_SHELL

+    # Overload the environment.
+    process.env = options.env
+
+    # Update the current working directory.
+    try
+      process.chdir options.env.PWD
+    catch err
+
     impromptu.load()
     impromptu.prompt.build (err, results) ->
       process.send